diff --git a/.appveyor.yml b/.appveyor.yml index 87f6cbde6384..2a641490520e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,7 +1,6 @@ # With infos from # http://tjelvarolsson.com/blog/how-to-continuously-test-your-python-code-on-windows-using-appveyor/ # https://packaging.python.org/en/latest/appveyor/ -# https://github.com/rmcgibbo/python-appveyor-conda-example --- # Backslashes in quotes need to be escaped: \ -> "\\" @@ -18,7 +17,7 @@ skip_commits: clone_depth: 50 -image: Visual Studio 2017 +image: Visual Studio 2019 environment: @@ -30,7 +29,6 @@ environment: matrix: - PYTHON_VERSION: "3.11" - CONDA_INSTALL_LOCN: "C:\\Miniconda3-x64" TEST_ALL: "yes" # We always use a 64-bit machine, but can build x86 distributions @@ -46,24 +44,21 @@ cache: - '%USERPROFILE%\.cache\matplotlib' init: - - echo %PYTHON_VERSION% %CONDA_INSTALL_LOCN% + - ps: + Invoke-Webrequest + -URI https://micro.mamba.pm/api/micromamba/win-64/latest + -OutFile C:\projects\micromamba.tar.bz2 + - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar.bz2 -aoa -oC:\projects\ + - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar -ttar -aoa -oC:\projects\ + - 'set PATH=C:\projects\Library\bin;%PATH%' + - micromamba shell init --shell cmd.exe + - micromamba config set always_yes true + - micromamba config prepend channels conda-forge + - micromamba info install: - - set PATH=%CONDA_INSTALL_LOCN%;%CONDA_INSTALL_LOCN%\scripts;%PATH%; - - conda config --set always_yes true - - conda config --set show_channel_urls yes - - conda config --prepend channels conda-forge - - # For building, use a new environment - # Add python version to environment - # `^ ` escapes spaces for indentation - - echo ^ ^ - python=%PYTHON_VERSION% >> environment.yml - - conda env create -f environment.yml - - activate mpl-dev - - conda install -c conda-forge pywin32 - - echo %PYTHON_VERSION% %TARGET_ARCH% - # Show the installed packages + versions - - conda list + - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 + - micromamba activate mpl-dev test_script: # Now build the thing.. @@ -74,7 +69,7 @@ test_script: - '"%DUMPBIN%" /DEPENDENTS lib\matplotlib\ft2font*.pyd | findstr freetype.*.dll && exit /b 1 || exit /b 0' # this are optional dependencies so that we don't skip so many tests... - - if x%TEST_ALL% == xyes conda install -q ffmpeg inkscape + - if x%TEST_ALL% == xyes micromamba install -q ffmpeg inkscape # miktex is available on conda, but seems to fail with permission errors. # missing packages on conda-forge for imagemagick # This install sometimes failed randomly :-( @@ -95,7 +90,7 @@ artifacts: type: Zip on_finish: - - conda install codecov + - micromamba install codecov - codecov -e PYTHON_VERSION PLATFORM -n "$PYTHON_VERSION Windows" on_failure: diff --git a/.circleci/config.yml b/.circleci/config.yml index eef9ca87f30d..e7348b868d4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,15 +98,17 @@ commands: parameters: numpy_version: type: string - default: "" + default: "~=2.0.0" steps: - run: name: Install Python dependencies command: | - python -m pip install --user meson-python numpy pybind11 setuptools-scm + python -m pip install --user -r requirements/dev/build-requirements.txt python -m pip install --user \ numpy<< parameters.numpy_version >> \ -r requirements/doc/doc-requirements.txt + python -m pip install --no-deps --user \ + git+https://github.com/matplotlib/mpl-sphinx-theme.git mpl-install: steps: @@ -145,7 +147,7 @@ commands: export RELEASE_TAG='-t release' fi mkdir -p logs - make html O="-T $RELEASE_TAG -j1 -w /tmp/sphinxerrorswarnings.log" + make html O="-T $RELEASE_TAG -j4 -w /tmp/sphinxerrorswarnings.log" rm -r build/html/_sources working_directory: doc - save_cache: @@ -214,9 +216,9 @@ commands: # jobs: - docs-python39: + docs-python3: docker: - - image: cimg/python:3.9 + - image: cimg/python:3.12 resource_class: large steps: - checkout @@ -257,4 +259,4 @@ workflows: jobs: # NOTE: If you rename this job, then you must update the `if` condition # and `circleci-jobs` option in `.github/workflows/circleci.yml`. - - docs-python39 + - docs-python3 diff --git a/.flake8 b/.flake8 index 36e8bcf5476f..7297d72b5841 100644 --- a/.flake8 +++ b/.flake8 @@ -34,17 +34,18 @@ exclude = per-file-ignores = lib/matplotlib/_cm.py: E202, E203, E302 - lib/matplotlib/_mathtext.py: E221, E251 - lib/matplotlib/_mathtext_data.py: E203, E261 + lib/matplotlib/_mathtext.py: E221 + lib/matplotlib/_mathtext_data.py: E203 lib/matplotlib/backends/backend_template.py: F401 lib/matplotlib/mathtext.py: E221 lib/matplotlib/pylab.py: F401, F403 lib/matplotlib/pyplot.py: F811 lib/matplotlib/tests/test_mathtext.py: E501 - lib/matplotlib/transforms.py: E201, E202, E203 + lib/matplotlib/transforms.py: E201, E202 lib/matplotlib/tri/_triinterpolate.py: E201, E221 lib/mpl_toolkits/axes_grid1/axes_size.py: E272 lib/mpl_toolkits/axisartist/angle_helper.py: E221 + lib/mpl_toolkits/mplot3d/proj3d.py: E201 doc/conf.py: E402 galleries/users_explain/quick_start.py: E402 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 613852425632..611431e707ab 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -12,3 +12,6 @@ c1a33a481b9c2df605bcb9bef9c19fe65c3dac21 # chore: pyupgrade --py39-plus 4d306402bb66d6d4c694d8e3e14b91054417070e + +# style: docstring parameter indentation +68daa962de5634753205cba27f21d6edff7be7a2 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 045386dc7402..0a57a3989265 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,9 @@ body: attributes: label: Code for reproduction description: >- - If possible, please provide a minimum self-contained example. + If possible, please provide a minimum self-contained example. If you + have used generative AI as an aid see + https://dev.matplotlib.org/devel/contributing.html#generative_ai. placeholder: Paste your code here. This field is automatically formatted as Python code. render: Python validations: diff --git a/.github/ISSUE_TEMPLATE/tag_proposal.yml b/.github/ISSUE_TEMPLATE/tag_proposal.yml index aa3345336089..2bb856d26be6 100644 --- a/.github/ISSUE_TEMPLATE/tag_proposal.yml +++ b/.github/ISSUE_TEMPLATE/tag_proposal.yml @@ -2,7 +2,7 @@ name: Tag Proposal description: Suggest a new tag or subcategory for the gallery of examples title: "[Tag]: " -labels: [Tag proposal] +labels: ["Documentation: tags"] body: - type: markdown attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f17a80dc4b6..dbf9e5f4ae22 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,13 +4,20 @@ out the development guide https://matplotlib.org/devdocs/devel/index.html --> ## PR summary - diff --git a/.github/labeler.yml b/.github/labeler.yml index 43a1246ba68a..75adfed57f43 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -89,6 +89,7 @@ - 'doc/conf.py' - 'doc/Makefile' - 'doc/make.bat' + - 'doc/sphinxext/**' "Documentation: devdocs": - changed-files: - any-glob-to-any-file: diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index aa0366ad16ff..b1e5204ab12a 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -39,14 +39,15 @@ jobs: SDIST_NAME: ${{ steps.sdist.outputs.SDIST_NAME }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 name: Install Python with: - python-version: 3.9 + python-version: '3.10' # Something changed somewhere that prevents the downloaded-at-build-time # licenses from being included in built wheels, so pre-download them so @@ -69,7 +70,7 @@ jobs: run: twine check dist/* - name: Upload sdist result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: cibw-sdist path: dist/*.tar.gz @@ -132,18 +133,18 @@ jobs: steps: - name: Set up QEMU if: matrix.cibw_archs == 'aarch64' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: arm64 - name: Download sdist - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: cibw-sdist path: dist/ - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -161,7 +162,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -169,7 +170,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -177,31 +178,31 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: CIBW_BUILD: "cp310-*" CIBW_ARCHS: ${{ matrix.cibw_archs }} - - name: Build wheels for CPython 3.9 - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 - with: - package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} - env: - CIBW_BUILD: "cp39-*" - CIBW_ARCHS: ${{ matrix.cibw_archs }} - - name: Build wheels for PyPy - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: - CIBW_BUILD: "pp39-*" + CIBW_BUILD: "pp310-*" CIBW_ARCHS: ${{ matrix.cibw_archs }} - if: matrix.cibw_archs != 'aarch64' + # Work around for https://github.com/pypa/setuptools/issues/4571 + # This can be removed once kiwisolver has wheels for PyPy 3.10 + # https://github.com/nucleic/kiwi/pull/182 + CIBW_BEFORE_TEST: >- + export PIP_CONSTRAINT=pypy-constraint.txt && + echo "setuptools!=72.2.0" > $PIP_CONSTRAINT && + pip install kiwisolver && + unset PIP_CONSTRAINT + if: matrix.cibw_archs != 'aarch64' && matrix.os != 'windows-latest' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: cibw-wheels-${{ runner.os }}-${{ matrix.cibw_archs }} path: ./wheelhouse/*.whl @@ -219,7 +220,7 @@ jobs: contents: read steps: - name: Download packages - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: cibw-* path: dist @@ -229,9 +230,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: dist/matplotlib-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index 3aead720cf20..e0ed6adf4e65 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -3,7 +3,7 @@ name: "CircleCI artifact handling" on: [status] jobs: circleci_artifacts_redirector_job: - if: "${{ github.event.context == 'ci/circleci: docs-python39' }}" + if: "${{ github.event.context == 'ci/circleci: docs-python3' }}" permissions: statuses: write runs-on: ubuntu-latest @@ -16,11 +16,11 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} api-token: ${{ secrets.CIRCLECI_TOKEN }} artifact-path: 0/doc/build/html/index.html - circleci-jobs: docs-python39 + circleci-jobs: docs-python3 job-title: View the built docs post_warnings_as_review: - if: "${{ github.event.context == 'ci/circleci: docs-python39' }}" + if: "${{ github.event.context == 'ci/circleci: docs-python3' }}" permissions: contents: read checks: write @@ -28,16 +28,20 @@ jobs: runs-on: ubuntu-latest name: Post warnings/errors as review steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Fetch result artifacts id: fetch-artifacts + env: + target_url: "${{ github.event.target_url }}" run: | - python .circleci/fetch_doc_logs.py "${{ github.event.target_url }}" + python .circleci/fetch_doc_logs.py "${target_url}" - name: Set up reviewdog if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" - uses: reviewdog/action-setup@v1 + uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0 with: reviewdog_version: latest diff --git a/.github/workflows/clean_pr.yml b/.github/workflows/clean_pr.yml index 77e49f7c1d9e..fc9021c920c0 100644 --- a/.github/workflows/clean_pr.yml +++ b/.github/workflows/clean_pr.yml @@ -10,9 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: '0' + persist-credentials: false - name: Check for added-and-deleted files run: | git fetch --quiet origin "$GITHUB_BASE_REF" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 203b0eee9ca4..4bf3e680f7b0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,10 +26,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: languages: ${{ matrix.language }} @@ -40,4 +42,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 diff --git a/.github/workflows/conflictcheck.yml b/.github/workflows/conflictcheck.yml index 3110839e5150..b018101f325c 100644 --- a/.github/workflows/conflictcheck.yml +++ b/.github/workflows/conflictcheck.yml @@ -9,12 +9,11 @@ on: pull_request_target: types: [synchronize] -permissions: - pull-requests: write - jobs: main: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Check if PRs have merge conflicts uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2 diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index a3f93ba195a8..bde902013412 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -48,10 +48,12 @@ jobs: test-cygwin: runs-on: windows-latest name: Python 3.${{ matrix.python-minor-version }} on Cygwin + # Enable these when Cygwin has Python 3.12. if: >- github.event_name == 'workflow_dispatch' || - github.event_name == 'schedule' || + (false && github.event_name == 'schedule') || ( + false && github.repository == 'matplotlib/matplotlib' && !contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]') && @@ -71,17 +73,18 @@ jobs: ) strategy: matrix: - python-minor-version: [9] + python-minor-version: [12] steps: - name: Fix line endings run: git config --global core.autocrlf input - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - - uses: cygwin/cygwin-install-action@v4 + - uses: cygwin/cygwin-install-action@006ad0b0946ca6d0a3ea2d4437677fa767392401 # v4 with: packages: >- ccache gcc-g++ gdb git graphviz libcairo-devel libffi-devel @@ -137,21 +140,21 @@ jobs: # FreeType build fails with bash, succeeds with dash - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 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@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 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@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 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 dde5bfb5ec81..d8664df9ba9a 100644 --- a/.github/workflows/do_not_merge.yml +++ b/.github/workflows/do_not_merge.yml @@ -23,7 +23,6 @@ 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 "${{env.has_tag}}" 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 8905511fc01d..cc15717e3351 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@v4 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index dc7a0716bfe8..8e2002353164 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@v5 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 with: sync-labels: true diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 969aacccad74..57acc3616ae6 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -4,22 +4,25 @@ on: [pull_request] permissions: contents: read - checks: write jobs: mypy-stubtest: name: mypy-stubtest runs-on: ubuntu-latest + permissions: + checks: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: - python-version: 3.9 + python-version: '3.10' - name: Set up reviewdog - uses: reviewdog/action-setup@v1 + uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.9 - name: Install tox run: python -m pip install tox @@ -30,7 +33,7 @@ jobs: run: | set -o pipefail tox -e stubtest | \ - sed -e "s!.tox/stubtest/lib/python3.9/site-packages!lib!g" | \ + sed -e "s!.tox/stubtest/lib/python3.10/site-packages!lib!g" | \ reviewdog \ -efm '%Eerror: %m' \ -efm '%CStub: in file %f:%l' \ diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 54e81f06b166..25e2bb344eda 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -59,7 +59,7 @@ jobs: ls -l dist/ - name: Upload wheels to Anaconda Cloud as nightlies - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/pr_welcome.yml b/.github/workflows/pr_welcome.yml index 533f676a0fab..3bb172ca70e7 100644 --- a/.github/workflows/pr_welcome.yml +++ b/.github/workflows/pr_welcome.yml @@ -3,15 +3,13 @@ name: PR Greetings on: [pull_request_target] -permissions: - pull-requests: write - jobs: greeting: runs-on: ubuntu-latest - + permissions: + pull-requests: write steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: >+ diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index fbd724571d80..24980f7a075b 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -4,26 +4,28 @@ on: [pull_request] permissions: contents: read - checks: write - pull-requests: write jobs: flake8: name: flake8 runs-on: ubuntu-latest + permissions: + checks: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: - python-version: 3.9 + python-version: '3.10' - name: Install flake8 run: pip3 install -r requirements/testing/flake8.txt - name: Set up reviewdog - uses: reviewdog/action-setup@v1 + uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.9 - name: Run flake8 env: @@ -36,19 +38,23 @@ jobs: mypy: name: mypy runs-on: ubuntu-latest + permissions: + checks: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: - python-version: 3.9 + python-version: '3.10' - name: Install mypy run: pip3 install -r requirements/testing/mypy.txt -r requirements/testing/all.txt - name: Set up reviewdog - uses: reviewdog/action-setup@v1 + uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.9 - name: Run mypy env: @@ -63,11 +69,15 @@ jobs: eslint: name: eslint runs-on: ubuntu-latest + permissions: + checks: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: eslint - uses: reviewdog/action-eslint@v1 + uses: reviewdog/action-eslint@9b5b0150e399e1f007ee3c27bc156549810a64e3 # v1.33.0 with: filter_mode: nofilter github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-tidy.yml b/.github/workflows/stale-tidy.yml index 92a81ee856e4..ab16c9f1fa1c 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@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 300 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c606d4288bd2..4dc964a0ea73 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 20 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dc216c6959f0..27aab4b392ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,31 +49,28 @@ jobs: include: - name-suffix: "(Minimum Versions)" os: ubuntu-20.04 - python-version: 3.9 + python-version: '3.10' extra-requirements: '-c requirements/testing/minver.txt' - pyqt5-ver: '==5.12.2 sip==5.0.0' # oldest versions with a Py3.9 wheel. - pyqt6-ver: '==6.1.0 PyQt6-Qt6==6.1.0' - pyside2-ver: '==5.15.1' # oldest version with working Py3.9 wheel. - pyside6-ver: '==6.0.0' delete-font-cache: true + # Oldest versions with Py3.10 wheels. + pyqt5-ver: '==5.15.5 sip==6.3.0' + pyqt6-ver: '==6.2.0 PyQt6-Qt6==6.2.0' + pyside2-ver: '==5.15.2.1' + pyside6-ver: '==6.2.0' - os: ubuntu-20.04 - python-version: 3.9 + python-version: '3.10' # One CI run tests ipython/matplotlib-inline before backend mapping moved to mpl - extra-requirements: '-r requirements/testing/extra.txt "ipython==7.19" "matplotlib-inline<0.1.7"' + extra-requirements: + -r requirements/testing/extra.txt + "ipython==7.29.0" + "ipykernel==5.5.6" + "matplotlib-inline<0.1.7" CFLAGS: "-fno-lto" # Ensure that disabling LTO works. # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954 # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - - os: ubuntu-20.04 - python-version: '3.10' - extra-requirements: '-r requirements/testing/extra.txt' - # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954 - # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html - pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1' - # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 - pyside6-ver: '!=6.5.1' - os: ubuntu-22.04 python-version: '3.11' # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html @@ -101,7 +98,7 @@ jobs: # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: macos-13 # This runner is on Intel chips. - python-version: '3.9' + python-version: '3.10' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: macos-14 # This runner is on M1 (arm64) chips. @@ -114,12 +111,13 @@ jobs: pyside6-ver: '!=6.5.1' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 if: matrix.python-version != '3.13t' with: python-version: ${{ matrix.python-version }} @@ -205,7 +203,7 @@ jobs: esac - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 if: startsWith(runner.os, 'Linux') with: path: ~/.cache/pip @@ -213,7 +211,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 if: startsWith(runner.os, 'macOS') with: path: ~/Library/Caches/pip @@ -221,7 +219,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache ccache - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: | ~/.ccache @@ -229,7 +227,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-ccache- - name: Cache Matplotlib - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: | ~/.cache/matplotlib @@ -256,8 +254,8 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - numpy packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ - 'meson-python>=0.13.1' 'pybind11>=2.6' \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + 'meson-python>=0.13.1' 'pybind11>=2.13.2' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} @@ -358,6 +356,40 @@ jobs: --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes + - name: Cleanup non-failed image files + if: failure() + run: | + function remove_files() { + local extension=$1 + find ./result_images -type f -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 + 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 + done + } + + remove_files "png"; remove_files "svg"; remove_files "pdf"; remove_files "eps"; + + if [ "$(find ./result_images -mindepth 1 -type d)" ]; then + find ./result_images/* -type d -empty -delete + fi + - name: Filter C coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} run: | @@ -376,12 +408,12 @@ jobs: fi - name: Upload code coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}" token: ${{ secrets.CODECOV_TOKEN }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: failure() with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }} result images" @@ -398,7 +430,7 @@ jobs: steps: - name: Create issue on failure - uses: imjohnbo/issue-bot@v3 + uses: imjohnbo/issue-bot@572eed14422c4d6ca37e870f97e7da209422f5bd # v3.4.4 with: title: "[TST] Upcoming dependency test failures" body: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14817e95929f..6068b8b0df83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ exclude: | ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -28,7 +28,7 @@ repos: - id: trailing-whitespace exclude_types: [svg] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: @@ -42,7 +42,7 @@ repos: files: lib/matplotlib # Only run when files in lib/matplotlib are changed. pass_filenames: false - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: @@ -51,7 +51,7 @@ repos: - flake8-force args: ["--docstring-convention=all"] - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell files: ^.*\.(py|c|cpp|h|m|md|rst|yml)$ @@ -67,7 +67,7 @@ repos: name: isort (python) files: ^galleries/tutorials/|^galleries/examples/|^galleries/plot_types/ - repo: https://github.com/rstcheck/rstcheck - rev: v6.2.0 + rev: v6.2.4 hooks: - id: rstcheck additional_dependencies: @@ -79,7 +79,7 @@ repos: - id: yamllint args: ["--strict", "--config-file=.yamllint.yml"] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.4 + rev: 0.29.3 hooks: # TODO: Re-enable this when https://github.com/microsoft/azure-pipelines-vscode/issues/567 is fixed. # - id: check-azure-pipelines diff --git a/SECURITY.md b/SECURITY.md index ce022ca60a0f..4400a4501b51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,14 +8,13 @@ versions. | Version | Supported | | ------- | ------------------ | +| 3.10.x | :white_check_mark: | | 3.9.x | :white_check_mark: | -| 3.8.x | :white_check_mark: | +| 3.8.x | :x: | | 3.7.x | :x: | | 3.6.x | :x: | | 3.5.x | :x: | -| 3.4.x | :x: | -| 3.3.x | :x: | -| < 3.3 | :x: | +| < 3.5 | :x: | ## Reporting a Vulnerability diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 00edc8e9a412..0c891fe42fef 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -49,29 +49,20 @@ stages: - job: Pytest strategy: matrix: - Linux_py39: - vmImage: 'ubuntu-20.04' # keep one job pinned to the oldest image - python.version: '3.9' Linux_py310: - vmImage: 'ubuntu-latest' + vmImage: 'ubuntu-20.04' # keep one job pinned to the oldest image python.version: '3.10' Linux_py311: vmImage: 'ubuntu-latest' python.version: '3.11' - macOS_py39: - vmImage: 'macOS-latest' - python.version: '3.9' macOS_py310: vmImage: 'macOS-latest' python.version: '3.10' macOS_py311: vmImage: 'macOS-latest' python.version: '3.11' - Windows_py39: - vmImage: 'windows-2019' # keep one job pinned to the oldest image - python.version: '3.9' Windows_py310: - vmImage: 'windows-latest' + vmImage: 'windows-2019' # keep one job pinned to the oldest image python.version: '3.10' Windows_py311: vmImage: 'windows-latest' @@ -148,7 +139,7 @@ stages: - bash: | python -m pip install --upgrade pip - python -m pip install --upgrade meson-python numpy pybind11 setuptools-scm + python -m pip install --upgrade -r requirements/dev/build-requirements.txt python -m pip install -r requirements/testing/all.txt -r requirements/testing/extra.txt displayName: 'Install dependencies with pip' diff --git a/ci/codespell-ignore-words.txt b/ci/codespell-ignore-words.txt index acbb2e8f39b5..5cd80beaa23c 100644 --- a/ci/codespell-ignore-words.txt +++ b/ci/codespell-ignore-words.txt @@ -16,3 +16,5 @@ te thisy whis wit +Copin +socio-economic diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 73dfb1d8ceb0..46ec06e0a9f1 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -1,48 +1,51 @@ # Non-typed (and private) modules/functions -matplotlib.backends.* -matplotlib.tests.* -matplotlib.pylab.* -matplotlib._.* -matplotlib.rcsetup._listify_validator -matplotlib.rcsetup._validate_linestyle -matplotlib.ft2font.Glyph -matplotlib.testing.jpl_units.* -matplotlib.sphinxext.* +matplotlib\.backends\..* +matplotlib\.tests(\..*)? +matplotlib\.pylab(\..*)? +matplotlib\._.* +matplotlib\.rcsetup\._listify_validator +matplotlib\.rcsetup\._validate_linestyle +matplotlib\.ft2font\.Glyph +matplotlib\.testing\.jpl_units\..* +matplotlib\.sphinxext(\..*)? # set methods have heavy dynamic usage of **kwargs, with differences for subclasses # which results in technically inconsistent signatures, but not actually a problem -matplotlib.*\.set$ +matplotlib\..*\.set$ # Typed inline, inconsistencies largely due to imports -matplotlib.pyplot.* -matplotlib.typing.* +matplotlib\.pyplot\..* +matplotlib\.typing\..* # Other decorator modifying signature # Backcompat decorator which does not modify runtime reported signature -matplotlib.offsetbox.*Offset[Bb]ox.get_offset +matplotlib\.offsetbox\..*Offset[Bb]ox\.get_offset # Inconsistent super/sub class parameter name (maybe rename for consistency) -matplotlib.projections.polar.RadialLocator.nonsingular -matplotlib.ticker.LogLocator.nonsingular -matplotlib.ticker.LogitLocator.nonsingular +matplotlib\.projections\.polar\.RadialLocator\.nonsingular +matplotlib\.ticker\.LogLocator\.nonsingular +matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) -matplotlib.backend_bases._Mode.__new__ -matplotlib.units.Number.__hash__ +matplotlib\.backend_bases\._Mode\.__new__ +matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations -matplotlib.figure.Figure.set_constrained_layout -matplotlib.figure.Figure.set_constrained_layout_pads -matplotlib.figure.Figure.set_tight_layout - -# positional-only argument name lacking leading underscores -matplotlib.axes._base._AxesBase.axis +matplotlib\.figure\.Figure\.set_constrained_layout +matplotlib\.figure\.Figure\.set_constrained_layout_pads +matplotlib\.figure\.Figure\.set_tight_layout # Maybe should be abstractmethods, required for subclasses, stubs define once -matplotlib.tri.*TriInterpolator.__call__ -matplotlib.tri.*TriInterpolator.gradient +matplotlib\.tri\..*TriInterpolator\.__call__ +matplotlib\.tri\..*TriInterpolator\.gradient # TypeVar used only in type hints -matplotlib.backend_bases.FigureCanvasBase._T -matplotlib.backend_managers.ToolManager._T -matplotlib.spines.Spine._T +matplotlib\.backend_bases\.FigureCanvasBase\._T +matplotlib\.backend_managers\.ToolManager\._T +matplotlib\.spines\.Spine\._T + +# Parameter inconsistency due to 3.10 deprecation +matplotlib\.figure\.FigureBase\.get_figure + +# getitem method only exists for 3.10 deprecation backcompatability +matplotlib\.inset\.InsetIndicator\.__getitem__ diff --git a/doc/Makefile b/doc/Makefile index 7eda39d87624..baed196a3ee2 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -18,6 +18,7 @@ help: clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) rm -rf "$(SOURCEDIR)/build" + rm -rf "$(SOURCEDIR)/_tags" rm -rf "$(SOURCEDIR)/api/_as_gen" rm -rf "$(SOURCEDIR)/gallery" rm -rf "$(SOURCEDIR)/plot_types" diff --git a/doc/_embedded_plots/axes_margins.py b/doc/_embedded_plots/axes_margins.py new file mode 100644 index 000000000000..d026840c3c15 --- /dev/null +++ b/doc/_embedded_plots/axes_margins.py @@ -0,0 +1,42 @@ +import numpy as np +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(6.5, 4)) +x = np.linspace(0, 1, 33) +y = -np.sin(x * 2*np.pi) +ax.plot(x, y, 'o') +ax.margins(0.5, 0.2) +ax.set_title("margins(x=0.5, y=0.2)") + +# fix the Axes limits so that the following helper drawings +# cannot change them further. +ax.set(xlim=ax.get_xlim(), ylim=ax.get_ylim()) + + +def arrow(p1, p2, **props): + ax.annotate("", p1, p2, + arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + + +axmin, axmax = ax.get_xlim() +aymin, aymax = ax.get_ylim() +xmin, xmax = x.min(), x.max() +ymin, ymax = y.min(), y.max() + +y0 = -0.8 +ax.axvspan(axmin, xmin, color=("orange", 0.1)) +ax.axvspan(xmax, axmax, color=("orange", 0.1)) +arrow((xmin, y0), (xmax, y0), color="sienna") +arrow((xmax, y0), (axmax, y0), color="orange") +ax.text((xmax + axmax)/2, y0+0.05, "x margin\n* x data range", + ha="center", va="bottom", color="orange") +ax.text(0.55, y0+0.1, "x data range", va="bottom", color="sienna") + +x0 = 0.1 +ax.axhspan(aymin, ymin, color=("tab:green", 0.1)) +ax.axhspan(ymax, aymax, color=("tab:green", 0.1)) +arrow((x0, ymin), (x0, ymax), color="darkgreen") +arrow((x0, ymax), (x0, aymax), color="tab:green") +ax.text(x0, (ymax + aymax) / 2, " y margin * y data range", + va="center", color="tab:green") +ax.text(x0, 0.5, " y data range", color="darkgreen") diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py new file mode 100644 index 000000000000..b4b8d7d32a3d --- /dev/null +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt + + +def arrow(p1, p2, **props): + axs[0, 0].annotate( + "", p1, p2, xycoords='figure fraction', + arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + + +fig, axs = plt.subplots(2, 2, figsize=(6.5, 4)) +fig.set_facecolor('lightblue') +fig.subplots_adjust(0.1, 0.1, 0.9, 0.9, 0.4, 0.4) +for ax in axs.flat: + ax.set(xticks=[], yticks=[]) + +arrow((0, 0.75), (0.1, 0.75)) # left +arrow((0.435, 0.75), (0.565, 0.75)) # wspace +arrow((0.9, 0.75), (1, 0.75)) # right +fig.text(0.05, 0.7, "left", ha="center") +fig.text(0.5, 0.7, "wspace", ha="center") +fig.text(0.95, 0.7, "right", ha="center") + +arrow((0.25, 0), (0.25, 0.1)) # bottom +arrow((0.25, 0.435), (0.25, 0.565)) # hspace +arrow((0.25, 0.9), (0.25, 1)) # top +fig.text(0.28, 0.05, "bottom", va="center") +fig.text(0.28, 0.5, "hspace", va="center") +fig.text(0.28, 0.95, "top", va="center") diff --git a/doc/_static/mpl.css b/doc/_static/mpl.css index aae167d1f6f8..9049ddbd8334 100644 --- a/doc/_static/mpl.css +++ b/doc/_static/mpl.css @@ -167,6 +167,14 @@ div.wide-table table th.stub { display: inline; } +/* sdd is a custom class that strips out styling from dropdowns + * Example usage: + * + * .. dropdown:: + * :class-container: sdd + * + */ + .sdd.sd-dropdown { box-shadow: none!important; } @@ -185,3 +193,17 @@ div.wide-table table th.stub { .sdd.sd-dropdown .sd-card-header +.sd-card-body{ --pst-sd-dropdown-color: none; } + +/* section-toc is a custom class that removes the page title from a toctree listing + * Example usage: + * + * .. rst-class:: section-toc + * .. toctree:: + * + */ +.section-toc.toctree-wrapper .toctree-l1>a{ + display: none; +} +.section-toc.toctree-wrapper li>ul{ + padding-inline-start:0; +} diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 3d712e4ff8e9..27451fd05657 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,15 +1,20 @@ [ { - "name": "3.9 (stable)", - "version": "stable", + "name": "3.10 (stable)", + "version": "3.10.0", "url": "https://matplotlib.org/stable/", "preferred": true }, { - "name": "3.10 (dev)", + "name": "3.11 (dev)", "version": "dev", "url": "https://matplotlib.org/devdocs/" }, + { + "name": "3.9", + "version": "3.9.3", + "url": "https://matplotlib.org/3.9.4/" + }, { "name": "3.8", "version": "3.8.4", diff --git a/doc/_static/transforms.png b/doc/_static/transforms.png deleted file mode 100644 index ab07fb575961..000000000000 Binary files a/doc/_static/transforms.png and /dev/null differ diff --git a/doc/_static/zenodo_cache/14436121.svg b/doc/_static/zenodo_cache/14436121.svg new file mode 100644 index 000000000000..1e4a7cd5b7a4 --- /dev/null +++ b/doc/_static/zenodo_cache/14436121.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.14436121 + + + 10.5281/zenodo.14436121 + + + \ No newline at end of file diff --git a/doc/_templates/autofunctions.rst b/doc/_templates/autofunctions.rst deleted file mode 100644 index 291b8eee2ede..000000000000 --- a/doc/_templates/autofunctions.rst +++ /dev/null @@ -1,22 +0,0 @@ - -{{ fullname | escape | underline }} - - -.. automodule:: {{ fullname }} - :no-members: - -{% block functions %} -{% if functions %} - -Functions ---------- - -.. autosummary:: - :template: autosummary.rst - :toctree: - -{% for item in functions %}{% if item not in ['plotting', 'colormaps'] %} - {{ item }}{% endif %}{% endfor %} - -{% endif %} -{% endblock %} diff --git a/doc/api/.gitignore b/doc/api/.gitignore new file mode 100644 index 000000000000..dbed88d89836 --- /dev/null +++ b/doc/api/.gitignore @@ -0,0 +1 @@ +scalarmappable.gen_rst diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index ac4e5bc4f536..6afdad4e768e 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -28,6 +28,17 @@ The Axes class Axes +Attributes +---------- + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + Axes.viewLim + Axes.dataLim + Plotting ======== diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 17e892b99df8..85ba990ffece 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -73,10 +73,10 @@ Axis Label :template: autosummary.rst :nosignatures: + Axis.label Axis.set_label_coords Axis.set_label_position Axis.set_label_text - Axis.get_label Axis.get_label_position Axis.get_label_text @@ -169,6 +169,8 @@ Units Axis.convert_units Axis.set_units Axis.get_units + Axis.set_converter + Axis.get_converter Axis.update_units @@ -217,6 +219,7 @@ Other Axis.axes Axis.limit_range_for_scale Axis.reset_ticks + Axis.set_clip_path Axis.set_default_intervals Discouraged @@ -234,6 +237,8 @@ specify a matching series of labels. Calling ``set_ticks`` makes a :template: autosummary.rst :nosignatures: + Axis.get_label + Axis.set_label Axis.set_ticks Axis.set_ticklabels @@ -263,8 +268,7 @@ specify a matching series of labels. Calling ``set_ticks`` makes a Tick.get_tick_padding Tick.get_tickdir Tick.get_view_interval - Tick.set_label1 - Tick.set_label2 + Tick.set_clip_path Tick.set_pad Tick.set_url Tick.update_position diff --git a/doc/api/cm_api.rst b/doc/api/cm_api.rst index 990d204c2a98..c9509389a2bb 100644 --- a/doc/api/cm_api.rst +++ b/doc/api/cm_api.rst @@ -6,3 +6,5 @@ :members: :undoc-members: :show-inheritance: + +.. include:: scalarmappable.gen_rst diff --git a/doc/api/colorizer_api.rst b/doc/api/colorizer_api.rst new file mode 100644 index 000000000000..e72da5cfb030 --- /dev/null +++ b/doc/api/colorizer_api.rst @@ -0,0 +1,9 @@ +************************ +``matplotlib.colorizer`` +************************ + +.. automodule:: matplotlib.colorizer + :members: + :undoc-members: + :show-inheritance: + :private-members: _ColorizerInterface, _ScalarMappable diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index 7ed2436d6661..6b02f723d74d 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -32,8 +32,8 @@ Color norms SymLogNorm TwoSlopeNorm -Colormaps ---------- +Univariate Colormaps +-------------------- .. autosummary:: :toctree: _as_gen/ @@ -43,6 +43,17 @@ Colormaps LinearSegmentedColormap ListedColormap +Multivariate Colormaps +---------------------- + +.. autosummary:: + :toctree: _as_gen/ + :template: autosummary.rst + + BivarColormap + SegmentedBivarColormap + BivarColormapFromImage + Other classes ------------- diff --git a/doc/api/index.rst b/doc/api/index.rst index 53f397a6817a..04c0e279a4fe 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -93,6 +93,7 @@ Alphabetical list of modules: cm_api.rst collections_api.rst colorbar_api.rst + colorizer_api.rst colors_api.rst container_api.rst contour_api.rst @@ -104,6 +105,7 @@ Alphabetical list of modules: gridspec_api.rst hatch_api.rst image_api.rst + inset_api.rst layout_engine_api.rst legend_api.rst legend_handler_api.rst diff --git a/doc/api/inset_api.rst b/doc/api/inset_api.rst new file mode 100644 index 000000000000..d8b89a106a7a --- /dev/null +++ b/doc/api/inset_api.rst @@ -0,0 +1,8 @@ +******************** +``matplotlib.inset`` +******************** + +.. automodule:: matplotlib.inset + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/next_api_changes.rst b/doc/api/next_api_changes.rst index d33c8014f735..9c0630697763 100644 --- a/doc/api/next_api_changes.rst +++ b/doc/api/next_api_changes.rst @@ -5,11 +5,40 @@ Next API changes .. ifconfig:: releaselevel == 'dev' + This page lists API changes for the next release. + + Behavior changes + ---------------- + .. toctree:: :glob: :maxdepth: 1 next_api_changes/behavior/* + + Deprecations + ------------ + + .. toctree:: + :glob: + :maxdepth: 1 + next_api_changes/deprecations/* - next_api_changes/development/* + + Removals + -------- + + .. toctree:: + :glob: + :maxdepth: 1 + next_api_changes/removals/* + + Development changes + ------------------- + + .. toctree:: + :glob: + :maxdepth: 1 + + next_api_changes/development/* diff --git a/doc/api/next_api_changes/README.rst b/doc/api/next_api_changes/README.rst index 75e70b456eb9..030a2644cdd4 100644 --- a/doc/api/next_api_changes/README.rst +++ b/doc/api/next_api_changes/README.rst @@ -1,10 +1,18 @@ :orphan: +.. NOTE TO EDITORS OF THIS FILE + This file serves as the README directly available in the file system next to the + next_api_changes entries. The content between the ``api-change-guide-*`` markers is + additionally included in the documentation page ``doc/devel/api_changes.rst``. Please + check that the page builds correctly after changing this file. + Adding API change notes ======================= -API change notes for future releases are collected in -:file:`next_api_changes`. They are divided into four subdirectories: +.. api-change-guide-start + +API change notes for future releases are collected in :file:`doc/api/next_api_changes/`. +They are divided into four subdirectories: - **Deprecations**: Announcements of future changes. Typically, these will raise a deprecation warning and users of this API should change their code @@ -33,6 +41,4 @@ Please avoid using references in section titles, as it causes links to be confusing in the table of contents. Instead, ensure that a reference is included in the descriptive text. -.. NOTE - Lines 5-30 of this file are include in :ref:`api_whats_new`; - therefore, please check the doc build after changing this file. +.. api-change-guide-end diff --git a/doc/api/next_api_changes/deprecations/27998-TS.rst b/doc/api/next_api_changes/deprecations/27998-TS.rst new file mode 100644 index 000000000000..ab69b87f0989 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/27998-TS.rst @@ -0,0 +1,7 @@ +``violinplot`` and ``violin`` *vert* parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter *vert: bool* has been deprecated on `~.Axes.violinplot` and +`~.Axes.violin`. +It will be replaced by *orientation: {"vertical", "horizontal"}* for API +consistency. diff --git a/doc/api/next_api_changes/deprecations/28074-TS.rst b/doc/api/next_api_changes/deprecations/28074-TS.rst new file mode 100644 index 000000000000..6a8b5d4b21b8 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28074-TS.rst @@ -0,0 +1,9 @@ +``boxplot`` and ``bxp`` *vert* parameter, and ``rcParams["boxplot.vertical"]`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter *vert: bool* has been deprecated on `~.Axes.boxplot` and +`~.Axes.bxp`. It is replaced by *orientation: {"vertical", "horizontal"}* +for API consistency. + +``rcParams["boxplot.vertical"]``, which controlled the orientation of ``boxplot``, +is deprecated without replacement. diff --git a/doc/api/prev_api_changes/api_changes_2.2.0.rst b/doc/api/prev_api_changes/api_changes_2.2.0.rst index 83369b66f8ab..404d0ca3ba38 100644 --- a/doc/api/prev_api_changes/api_changes_2.2.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.2.0.rst @@ -61,7 +61,7 @@ the future, only broadcasting (1 column to *n* columns) will be performed. rcparams ~~~~~~~~ -The :rc:`backend.qt4` and :rc:`backend.qt5` rcParams were deprecated +The ``backend.qt4`` and ``backend.qt5`` rcParams were deprecated in version 2.2. In order to force the use of a specific Qt binding, either import that binding first, or set the ``QT_API`` environment variable. diff --git a/doc/api/prev_api_changes/api_changes_3.1.0.rst b/doc/api/prev_api_changes/api_changes_3.1.0.rst index 5b06af781938..365476f54e3c 100644 --- a/doc/api/prev_api_changes/api_changes_3.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.1.0.rst @@ -743,8 +743,8 @@ The following signature related behaviours are deprecated: `.Axes.annotate()` instead. - Passing (n, 1)-shaped error arrays to `.Axes.errorbar()`, which was not documented and did not work for ``n = 2``. Pass a 1D array instead. -- The *frameon* kwarg to `~.Figure.savefig` and the :rc:`savefig.frameon` rcParam. - To emulate ``frameon = False``, set *facecolor* to fully +- The *frameon* keyword argument to `~.Figure.savefig` and the ``savefig.frameon`` + rcParam. To emulate ``frameon = False``, set *facecolor* to fully transparent (``"none"``, or ``(0, 0, 0, 0)``). - Passing a non-1D (typically, (n, 1)-shaped) input to `.Axes.pie`. Pass a 1D array instead. diff --git a/doc/api/prev_api_changes/api_changes_3.10.0.rst b/doc/api/prev_api_changes/api_changes_3.10.0.rst new file mode 100644 index 000000000000..83bde66213f3 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.0.rst @@ -0,0 +1,14 @@ +API Changes for 3.10.0 +====================== + +.. contents:: + :local: + :depth: 1 + +.. include:: /api/prev_api_changes/api_changes_3.10.0/behaviour.rst + +.. include:: /api/prev_api_changes/api_changes_3.10.0/deprecations.rst + +.. include:: /api/prev_api_changes/api_changes_3.10.0/removals.rst + +.. include:: /api/prev_api_changes/api_changes_3.10.0/development.rst diff --git a/doc/api/prev_api_changes/api_changes_3.10.0/behavior.rst b/doc/api/prev_api_changes/api_changes_3.10.0/behavior.rst new file mode 100644 index 000000000000..87da6568a860 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.0/behavior.rst @@ -0,0 +1,118 @@ +onselect argument to selector widgets made optional +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *onselect* argument to `.EllipseSelector`, `.LassoSelector`, `.PolygonSelector`, and +`.RectangleSelector` is no longer required. + +``NavigationToolbar2.save_figure`` now returns filepath of saved figure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``NavigationToolbar2.save_figure`` function may return the filename of the saved figure. + +If a backend implements this functionality it should return `None` +in the case where no figure is actually saved (because the user closed the dialog without saving). + +If the backend does not or can not implement this functionality (currently the Gtk4 backends +and webagg backends do not) this method will return ``NavigationToolbar2.UNKNOWN_SAVED_STATUS``. + +SVG output: improved reproducibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some SVG-format plots `produced different output on each render `__, even with a static ``svg.hashsalt`` value configured. + +The problem was a non-deterministic ID-generation scheme for clip paths; the fix introduces a repeatable, monotonically increasing integer ID scheme as a replacement. + +Provided that plots add clip paths themselves in deterministic order, this enables repeatable (a.k.a. reproducible, deterministic) SVG output. + +ft2font classes are now final +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ft2font classes `.ft2font.FT2Font`, and `.ft2font.FT2Image` are now final +and can no longer be subclassed. + +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator`. Use the +`~matplotlib.inset.InsetIndicator.rectangle` and +`~matplotlib.inset.InsetIndicator.connectors` properties of this artist to +access the objects that were previously returned directly. + +``imshow`` *interpolation_stage* default changed to 'auto' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *interpolation_stage* parameter of `~.Axes.imshow` has a new default +value 'auto'. For images that are up-sampled less than a factor of +three or down-sampled, image interpolation will occur in 'rgba' space. For images +that are up-sampled by a factor of 3 or more, then image interpolation occurs +in 'data' space. + +The previous default was 'data', so down-sampled images may change subtly with +the new default. However, the new default also avoids floating point artifacts +at sharp boundaries in a colormap when down-sampling. + +The previous behavior can achieved by setting the *interpolation_stage* parameter +or :rc:`image.interpolation_stage` to 'data'. + +imshow default *interpolation* changed to 'auto' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *interpolation* parameter of `~.Axes.imshow` has a new default +value 'auto', changed from 'antialiased', for consistency with *interpolation_stage* +and because the interpolation is only anti-aliasing during down-sampling. Passing +'antialiased' still works, and behaves exactly the same as 'auto', but is discouraged. + +dark_background and fivethirtyeight styles no longer set ``savefig.facecolor`` and ``savefig.edgecolor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using these styles, :rc:`savefig.facecolor` and :rc:`savefig.edgecolor` +now inherit the global default value of "auto", which means that the actual +figure colors will be used. Previously, these rcParams were set to the same +values as :rc:`figure.facecolor` and :rc:`figure.edgecolor`, i.e. a saved +figure would always use the theme colors even if the user manually overrode +them; this is no longer the case. + +This change should have no impact for users that do not manually set the figure +face and edge colors. + +Add zorder option in QuiverKey +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``zorder`` can be used as a keyword argument to `.QuiverKey`. Previously, +that parameter did not have any effect because the zorder was hard coded. + +Subfigures +~~~~~~~~~~ + +`.Figure.subfigures` are now added in row-major order to be consistent with +`.Figure.subplots`. The return value of `~.Figure.subfigures` is not changed, +but the order of ``fig.subfigs`` is. + +(Sub)Figure.get_figure +~~~~~~~~~~~~~~~~~~~~~~ + +...in future will by default return the direct parent figure, which may be a SubFigure. +This will make the default behavior consistent with the +`~matplotlib.artist.Artist.get_figure` method of other artists. To control the +behavior, use the newly introduced *root* parameter. + + +``transforms.AffineDeltaTransform`` updates correctly on axis limit changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before this change, transform sub-graphs with ``AffineDeltaTransform`` did not update correctly. +This PR ensures that changes to the child transform are passed through correctly. + +The offset string associated with ConciseDateFormatter will now invert when the axis is inverted +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, when the axis was inverted, the offset string associated with ConciseDateFormatter would not change, +so the offset string indicated the axis was oriented in the wrong direction. Now, when the axis is inverted, the offset +string is oriented correctly. + +``suptitle`` in compressed layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Compressed layout now automatically positions the `~.Figure.suptitle` just +above the top row of axes. To keep this title in its previous position, +either pass ``in_layout=False`` or explicitly set ``y=0.98`` in the +`~.Figure.suptitle` call. diff --git a/doc/api/prev_api_changes/api_changes_3.10.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.10.0/deprecations.rst new file mode 100644 index 000000000000..ad344b37d069 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.0/deprecations.rst @@ -0,0 +1,164 @@ +Positional parameters in plotting functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many plotting functions will restrict positional arguments to the first few parameters +in the future. All further configuration parameters will have to be passed as keyword +arguments. This is to enforce better code and and allow for future changes with reduced +risk of breaking existing code. +Changing ``Figure.number`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Changing ``Figure.number`` is deprecated. This value is used by `.pyplot` +to identify figures. It must stay in sync with the pyplot internal state +and is not intended to be modified by the user. + +``PdfFile.hatchPatterns`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. + +(Sub)Figure.set_figure +~~~~~~~~~~~~~~~~~~~~~~ + +...is deprecated and in future will always raise an exception. The parent and +root figures of a (Sub)Figure are set at instantiation and cannot be changed. + +``Poly3DCollection.get_vector`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. + +Deprecated ``register`` on ``matplotlib.patches._Styles`` and subclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This class method is never used internally. Due to the internal check in the +method it only accepts subclasses of a private baseclass embedded in the host +class which makes it unlikely that it has been used externally. + +matplotlib.validate_backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...is deprecated. Please use `matplotlib.rcsetup.validate_backend` instead. + + +matplotlib.sanitize_sequence +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...is deprecated. Please use `matplotlib.cbook.sanitize_sequence` instead. + +ft2font module-level constants replaced by enums +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.ft2font`-level constants have been converted to `enum` classes, and all API using +them now take/return the new types. + +The following constants are now part of `.ft2font.Kerning` (without the ``KERNING_`` +prefix): + +- ``KERNING_DEFAULT`` +- ``KERNING_UNFITTED`` +- ``KERNING_UNSCALED`` + +The following constants are now part of `.ft2font.LoadFlags` (without the ``LOAD_`` +prefix): + +- ``LOAD_DEFAULT`` +- ``LOAD_NO_SCALE`` +- ``LOAD_NO_HINTING`` +- ``LOAD_RENDER`` +- ``LOAD_NO_BITMAP`` +- ``LOAD_VERTICAL_LAYOUT`` +- ``LOAD_FORCE_AUTOHINT`` +- ``LOAD_CROP_BITMAP`` +- ``LOAD_PEDANTIC`` +- ``LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH`` +- ``LOAD_NO_RECURSE`` +- ``LOAD_IGNORE_TRANSFORM`` +- ``LOAD_MONOCHROME`` +- ``LOAD_LINEAR_DESIGN`` +- ``LOAD_NO_AUTOHINT`` +- ``LOAD_TARGET_NORMAL`` +- ``LOAD_TARGET_LIGHT`` +- ``LOAD_TARGET_MONO`` +- ``LOAD_TARGET_LCD`` +- ``LOAD_TARGET_LCD_V`` + +The following constants are now part of `.ft2font.FaceFlags`: + +- ``EXTERNAL_STREAM`` +- ``FAST_GLYPHS`` +- ``FIXED_SIZES`` +- ``FIXED_WIDTH`` +- ``GLYPH_NAMES`` +- ``HORIZONTAL`` +- ``KERNING`` +- ``MULTIPLE_MASTERS`` +- ``SCALABLE`` +- ``SFNT`` +- ``VERTICAL`` + +The following constants are now part of `.ft2font.StyleFlags`: + +- ``ITALIC`` +- ``BOLD`` + +FontProperties initialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.FontProperties` initialization is limited to the two call patterns: + +- single positional parameter, interpreted as fontconfig pattern +- only keyword parameters for setting individual properties + +All other previously supported call patterns are deprecated. + +``AxLine`` ``xy1`` and ``xy2`` setters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These setters now each take a single argument, ``xy1`` or ``xy2`` as a tuple. +The old form, where ``x`` and ``y`` were passed as separate arguments, is +deprecated. + +Calling ``pyplot.polar()`` with an existing non-polar Axes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This currently plots the data into the non-polar Axes, ignoring +the "polar" intention. This usage scenario is deprecated and +will raise an error in the future. + +Passing floating-point values to ``RendererAgg.draw_text_image`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any floating-point values passed to the *x* and *y* parameters were truncated to integers +silently. This behaviour is now deprecated, and only `int` values should be used. + +Passing floating-point values to ``FT2Image`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any floating-point values passed to the `.FT2Image` constructor, or the *x0*, *y0*, *x1*, +and *y1* parameters of `.FT2Image.draw_rect_filled` were truncated to integers silently. +This behaviour is now deprecated, and only `int` values should be used. + +``boxplot`` and ``bxp`` *vert* parameter, and ``rcParams["boxplot.vertical"]`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter *vert: bool* has been deprecated on `~.Axes.boxplot` and +`~.Axes.bxp`. It is replaced by *orientation: {"vertical", "horizontal"}* +for API consistency. + +``rcParams["boxplot.vertical"]``, which controlled the orientation of ``boxplot``, +is deprecated without replacement. + +This deprecation is currently marked as pending and will be fully deprecated in Matplotlib 3.11. + +``violinplot`` and ``violin`` *vert* parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter *vert: bool* has been deprecated on `~.Axes.violinplot` and +`~.Axes.violin`. +It will be replaced by *orientation: {"vertical", "horizontal"}* for API +consistency. + +This deprecation is currently marked as pending and will be fully deprecated in Matplotlib 3.11. + +``proj3d.proj_transform_clip`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. diff --git a/doc/api/prev_api_changes/api_changes_3.10.0/development.rst b/doc/api/prev_api_changes/api_changes_3.10.0/development.rst new file mode 100644 index 000000000000..58ece9877912 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.0/development.rst @@ -0,0 +1,22 @@ +Documentation-specific custom Sphinx roles are now semi-public +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For third-party packages that derive types from Matplotlib, our use of custom roles may +prevent Sphinx from building their docs. These custom Sphinx roles are now public solely +for the purposes of use within projects that derive from Matplotlib types. See +:mod:`matplotlib.sphinxext.roles` for details. + +Increase to minimum supported versions of dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For Matplotlib 3.10, the :ref:`minimum supported versions ` are +being bumped: + ++------------+-----------------+----------------+ +| Dependency | min in mpl3.9 | min in mpl3.10 | ++============+=================+================+ +| Python | 3.9 | 3.10 | ++------------+-----------------+----------------+ + +This is consistent with our :ref:`min_deps_policy` and `SPEC0 +`__ diff --git a/doc/api/prev_api_changes/api_changes_3.10.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.10.0/removals.rst new file mode 100644 index 000000000000..e535123c7016 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.0/removals.rst @@ -0,0 +1,303 @@ +ttconv removed +~~~~~~~~~~~~~~ + +The ``matplotlib._ttconv`` extension has been removed. Most of its +functionaliy was already replaced by other code, and the only thing left +was embedding TTF fonts in PostScript in Type 42 format. This is now +done in the PS backend using the FontTools library. + +Remove hard reference to ``lastevent`` in ``LocationEvent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +This was previously used to detect exiting from axes, however the hard +reference would keep closed `.Figure` objects and their children alive longer +than expected. + +``ft2font.FT2Image.draw_rect`` and ``ft2font.FT2Font.get_xys`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been removed as they are unused. + +``Tick.set_label``, ``Tick.set_label1`` and ``Tick.set_label2`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are removed. Calling these methods from third-party code usually had no +effect, as the labels are overwritten at draw time by the tick formatter. + + +Functions in ``mpl_toolkits.mplot3d.proj3d`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The function ``transform`` is just an alias for ``proj_transform``, +use the latter instead. + +The following functions were either unused (so no longer required in Matplotlib) +or considered private. + +* ``ortho_transformation`` +* ``persp_transformation`` +* ``proj_points`` +* ``proj_trans_points`` +* ``rot_x`` +* ``rotation_about_vector`` +* ``view_transformation`` + + +Arguments other than ``renderer`` to ``get_tightbbox`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are keyword-only arguments. This is for consistency and that +different classes have different additional arguments. + + +Method parameters renamed to match base classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The only parameter of ``transform_affine`` and ``transform_non_affine`` in ``Transform`` subclasses is renamed +to *values*. + +The *points* parameter of ``transforms.IdentityTransform.transform`` is renamed to *values*. + +The *trans* parameter of ``table.Cell.set_transform`` is renamed to *t* consistently with +`.Artist.set_transform`. + +The *clippath* parameters of ``axis.Axis.set_clip_path`` and ``axis.Tick.set_clip_path`` are +renamed to *path* consistently with `.Artist.set_clip_path`. + +The *s* parameter of ``images.NonUniformImage.set_filternorm`` is renamed to *filternorm* +consistently with ``_ImageBase.set_filternorm``. + +The *s* parameter of ``images.NonUniformImage.set_filterrad`` is renamed to *filterrad* +consistently with ``_ImageBase.set_filterrad``. + +The only parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* +consistently with `.Artist.contains`. + +Method parameters renamed +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *p* parameter of ``BboxBase.padded`` is renamed to *w_pad*, consistently with the other parameter, *h_pad* + +*numdecs* parameter and attribute of ``LogLocator`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are removed without replacement, because they had no effect. +The ``PolyQuadMesh`` class requires full 2D arrays of values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, if a masked array was input, the list of polygons within the collection +would shrink to the size of valid polygons and users were required to keep track of +which polygons were drawn and call ``set_array()`` with the smaller "compressed" +array size. Passing the "compressed" and flattened array values will no longer +work and the full 2D array of values (including the mask) should be passed +to `.PolyQuadMesh.set_array`. +``ContourSet.collections`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. `~.ContourSet` is now implemented as a single +`~.Collection` of paths, each path corresponding to a contour level, possibly +including multiple unconnected components. + +``ContourSet.antialiased`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. Use `~.Collection.get_antialiased` or +`~.Collection.set_antialiased` instead. Note that `~.Collection.get_antialiased` +returns an array. + +``tcolors`` and ``tlinewidths`` attributes of ``ContourSet`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been removed. Use `~.Collection.get_facecolor`, `~.Collection.get_edgecolor` +or `~.Collection.get_linewidths` instead. + + +``calc_label_rot_and_inline`` method of ``ContourLabeler`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed without replacement. + + +``add_label_clabeltext`` method of ``ContourLabeler`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. Use `~.ContourLabeler.add_label` instead. +Passing extra positional arguments to ``Figure.add_axes`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Positional arguments passed to `.Figure.add_axes` other than a rect or an existing +``Axes`` were previously ignored, and is now an error. + + +Artists explicitly passed in will no longer be filtered by legend() based on their label +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, artists explicitly passed to ``legend(handles=[...])`` are filtered out if +their label starts with an underscore. This filter is no longer applied; explicitly +filter out such artists (``[art for art in artists if not +art.get_label().startswith('_')]``) if necessary. + +Note that if no handles are specified at all, then the default still filters out labels +starting with an underscore. + + +The parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... consistently with `.Artist.contains`. + + +Support for passing the "frac" key in ``annotate(..., arrowprops={"frac": ...})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. This key has had no effect since Matplotlib 1.5. + + +Passing non-int or sequence of non-int to ``Table.auto_set_column_width`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Column numbers are ints, and formerly passing any other type was effectively ignored. +This has now become an error. + + +Widgets +~~~~~~~ + +The *visible* attribute getter of ``*Selector`` widgets has been removed; use +``get_visible`` instead. + + +Auto-closing of figures when switching backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Allowable backend switches (i.e. those that do not swap a GUI event loop with another +one) will not close existing figures. If necessary, call ``plt.close("all")`` before +switching. + + +``FigureCanvasBase.switch_backends`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed with no replacement. + + +Accessing ``event.guiEvent`` after event handlers return +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is no longer supported, and ``event.guiEvent`` will be set to None once the event +handlers return. For some GUI toolkits, it is unsafe to use the event, though you may +separately stash the object at your own risk. + + +``PdfPages(keep_empty=True)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A zero-page PDF is not valid, thus passing ``keep_empty=True`` to `.backend_pdf.PdfPages` +and `.backend_pgf.PdfPages`, and the ``keep_empty`` attribute of these classes, is no +longer allowed, and empty PDF files will not be created. + +Furthermore, `.backend_pdf.PdfPages` no longer immediately creates the target file upon +instantiation, but only when the first figure is saved. To fully control file creation, +directly pass an opened file object as argument (``with open(path, "wb") as file, +PdfPages(file) as pdf: ...``). + + +``backend_ps.psDefs`` +~~~~~~~~~~~~~~~~~~~~~ + +The ``psDefs`` module-level variable in ``backend_ps`` has been removed with no +replacement. + + +Automatic papersize selection in PostScript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting :rc:`ps.papersize` to ``'auto'`` or passing ``papersize='auto'`` to +`.Figure.savefig` is no longer supported. Either pass an explicit paper type name, or +omit this parameter to use the default from the rcParam. + + +``RendererAgg.tostring_rgb`` and ``FigureCanvasAgg.tostring_rgb`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been remove with no direct replacement. Consider using ``buffer_rgba`` instead, +which should cover most use cases. + + +``NavigationToolbar2QT.message`` has been removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... with no replacement. + + +``TexManager.texcache`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... is considered private and has been removed. The location of the cache directory is +clarified in the doc-string. + + +``cbook`` API changes +~~~~~~~~~~~~~~~~~~~~~ + +``cbook.Stack`` has been removed with no replacement. + +``Grouper.clean()`` has been removed with no replacement. The Grouper class now cleans +itself up automatically. + +The *np_load* parameter of ``cbook.get_sample_data`` has been removed; `.get_sample_data` +now auto-loads numpy arrays. Use ``get_sample_data(..., asfileobj=False)`` instead to get +the filename of the data file, which can then be passed to `open`, if desired. + + +Calling ``paths.get_path_collection_extents`` with empty *offsets* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Calling `~.get_path_collection_extents` with an empty *offsets* parameter has an +ambiguous interpretation and is no longer allowed. + + +``bbox.anchored()`` with no explicit container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not passing a *container* argument to `.BboxBase.anchored` is no longer supported. + + +``INVALID_NON_AFFINE``, ``INVALID_AFFINE``, ``INVALID`` attributes of ``TransformNode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These attributes have been removed. + + +``axes_grid1`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``anchored_artists.AnchoredEllipse`` has been removed. Instead, directly construct an +`.AnchoredOffsetbox`, an `.AuxTransformBox`, and an `~.patches.Ellipse`, as demonstrated +in :doc:`/gallery/misc/anchored_artists`. + +The ``axes_divider.AxesLocator`` class has been removed. The ``new_locator`` method of +divider instances now instead returns an opaque callable (which can still be passed to +``ax.set_axes_locator``). + +``axes_divider.Divider.locate`` has been removed; use ``Divider.new_locator(...)(ax, +renderer)`` instead. + +``axes_grid.CbarAxesBase.toggle_label`` has been removed. Instead, use standard methods +for manipulating colorbar labels (`.Colorbar.set_label`) and tick labels +(`.Axes.tick_params`). + +``inset_location.InsetPosition`` has been removed; use `~.Axes.inset_axes` instead. + + +``axisartist`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``axisartist.axes_grid`` and ``axisartist.axes_rgb`` modules, which provide wrappers +combining the functionality of `.axes_grid1` and `.axisartist`, have been removed; +directly use e.g. ``AxesGrid(..., axes_class=axislines.Axes)`` instead. + +Calling an axisartist Axes to mean `~matplotlib.pyplot.axis` has been removed; explicitly +call the method instead. + +``floating_axes.GridHelperCurveLinear.get_data_boundary`` has been removed. Use +``grid_finder.extreme_finder(*[None] * 5)`` to get the extremes of the grid. diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst index 8e4c1e81f9ec..53d76d667509 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst @@ -61,7 +61,7 @@ The following API elements have been removed: - passing ``(verts, 0)`` or ``(..., 3)`` when specifying a marker to specify a path or a circle, respectively (instead, use ``verts`` or ``"o"``, respectively) -- :rc:`examples.directory` +- the ``examples.directory`` rcParam The following members of ``matplotlib.backends.backend_pdf.PdfFile`` were removed: diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst index 256c33ed762f..76c43b12aaaa 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst @@ -83,8 +83,8 @@ Passing both singular and plural *colors*, *linewidths*, *linestyles* to `.Axes. Passing e.g. both *linewidth* and *linewidths* will raise a TypeError in the future. -Setting :rc:`text.latex.preamble` or :rc:`pdf.preamble` to non-strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setting ``text.latex.preamble`` or ``pdf.preamble`` rcParams to non-strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These rcParams should be set to string values. Support for None (meaning the empty string) and lists of strings (implicitly joined with newlines) is deprecated. @@ -311,7 +311,7 @@ JPEG options ~~~~~~~~~~~~ The *quality*, *optimize*, and *progressive* keyword arguments to `~.Figure.savefig`, which were only used when saving to JPEG, are deprecated. -:rc:`savefig.jpeg_quality` is likewise deprecated. +The ``savefig.jpeg_quality`` rcParam is likewise deprecated. Such options should now be directly passed to Pillow using ``savefig(..., pil_kwargs={"quality": ..., "optimize": ..., "progressive": ...})``. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst index 0dcf76cbbe7a..3acab92c3577 100644 --- a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst @@ -359,7 +359,6 @@ rcParams - :rc:`axes.axisbelow` no longer accepts strings starting with "line" (case-insensitive) as "line"; use "line" (case-sensitive) instead. - - :rc:`text.latex.preamble` and :rc:`pdf.preamble` no longer accept - non-string values. + - :rc:`text.latex.preamble` and ``pdf.preamble`` no longer accept non-string values. - All ``*.linestyle`` rcParams no longer accept ``offset = None``; set the offset to 0 instead. diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst index 91802692ebb4..6ace010515fb 100644 --- a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst @@ -241,7 +241,7 @@ Specified exception types in ``Grid`` In a few cases an `Exception` was thrown when an incorrect argument value was set in the `mpl_toolkits.axes_grid1.axes_grid.Grid` (= -`mpl_toolkits.axisartist.axes_grid.Grid`) constructor. These are replaced as +``mpl_toolkits.axisartist.axes_grid.Grid``) constructor. These are replaced as follows: * Providing an incorrect value for *ngrids* now raises a `ValueError` diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst index dd6d9d8e0894..55a0a7133c65 100644 --- a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst @@ -90,7 +90,7 @@ Passing undefined *label_mode* to ``Grid`` ... is deprecated. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding -classes imported from `mpl_toolkits.axisartist.axes_grid`. +classes imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst index 3476a05394df..0b598723e26c 100644 --- a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst @@ -159,10 +159,10 @@ Passing ``None`` as ``rotation_mode`` to `.Text` (the default value) or passing `.Text.set_rotation_mode` will make `.Text.get_rotation_mode` return ``"default"`` instead of ``None``. The behaviour otherwise is the same. -PostScript paper type adds option to use figure size +PostScript paper size adds option to use figure size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :rc:`ps.papertype` rcParam can now be set to ``'figure'``, which will use +The :rc:`ps.papersize` rcParam can now be set to ``'figure'``, which will use a paper size that corresponds exactly with the size of the figure that is being saved. diff --git a/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst index b442a4af51dc..5398cec623b9 100644 --- a/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst @@ -153,10 +153,10 @@ The *clippath* parameters of ``axis.Axis.set_clip_path`` and ``axis.Tick.set_cl renamed to *path* consistently with `.Artist.set_clip_path`. The *s* parameter of ``images.NonUniformImage.set_filternorm`` is renamed to *filternorm* -consistently with ```_ImageBase.set_filternorm``. +consistently with ``_ImageBase.set_filternorm``. The *s* parameter of ``images.NonUniformImage.set_filterrad`` is renamed to *filterrad* -consistently with ```_ImageBase.set_filterrad``. +consistently with ``_ImageBase.set_filterrad``. *numdecs* parameter and attribute of ``LogLocator`` diff --git a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst index b9aa03cfbf92..791c04149981 100644 --- a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst @@ -111,7 +111,7 @@ Passing undefined *label_mode* to ``Grid`` ... is no longer allowed. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding classes -imported from `mpl_toolkits.axisartist.axes_grid`. +imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/testing_api.rst b/doc/api/testing_api.rst index 7731d4510b27..ae81d2f89ca7 100644 --- a/doc/api/testing_api.rst +++ b/doc/api/testing_api.rst @@ -37,3 +37,11 @@ :members: :undoc-members: :show-inheritance: + + +Testing with optional dependencies +================================== +For more information on fixtures, see :external+pytest:ref:`pytest fixtures `. + +.. autofunction:: matplotlib.testing.conftest.pd +.. autofunction:: matplotlib.testing.conftest.xr diff --git a/doc/api/toolkits/axisartist.rst b/doc/api/toolkits/axisartist.rst index 8cac4d68a266..5f58d134d370 100644 --- a/doc/api/toolkits/axisartist.rst +++ b/doc/api/toolkits/axisartist.rst @@ -34,8 +34,6 @@ You can find a tutorial describing usage of axisartist at the axisartist.angle_helper axisartist.axes_divider - axisartist.axes_grid - axisartist.axes_rgb axisartist.axis_artist axisartist.axisline_style axisartist.axislines diff --git a/doc/api/toolkits/mplot3d.rst b/doc/api/toolkits/mplot3d.rst index f14918314b97..0d860bd2cfea 100644 --- a/doc/api/toolkits/mplot3d.rst +++ b/doc/api/toolkits/mplot3d.rst @@ -118,12 +118,6 @@ the toolbar pan and zoom buttons are not used. :template: autosummary.rst proj3d.inv_transform - proj3d.persp_transformation - proj3d.proj_points - proj3d.proj_trans_points proj3d.proj_transform proj3d.proj_transform_clip - proj3d.rot_x - proj3d.transform - proj3d.view_transformation proj3d.world_transformation diff --git a/doc/api/toolkits/mplot3d/axes3d.rst b/doc/api/toolkits/mplot3d/axes3d.rst index 877e47b7e93a..83cd8dd63cef 100644 --- a/doc/api/toolkits/mplot3d/axes3d.rst +++ b/doc/api/toolkits/mplot3d/axes3d.rst @@ -30,6 +30,7 @@ Plotting plot_surface plot_wireframe plot_trisurf + fill_between clabel contour diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index 10d4fac39e8c..75b24ba9c7b0 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -12,8 +12,8 @@ The position of the viewport "camera" in a 3D plot is defined by three angles: points towards the center of the plot box volume. The angle direction is a common convention, and is shared with `PyVista `_ and -`MATLAB `_ -(though MATLAB lacks a roll angle). Note that a positive roll angle rotates the +`MATLAB `_. +Note that a positive roll angle rotates the viewing plane clockwise, so the 3d axes will appear to rotate counter-clockwise. @@ -21,8 +21,8 @@ counter-clockwise. :align: center :scale: 50 -Rotating the plot using the mouse will control only the azimuth and elevation, -but all three angles can be set programmatically:: +Rotating the plot using the mouse will control azimuth, elevation, +as well as roll, and all three angles can be set programmatically:: import matplotlib.pyplot as plt ax = plt.figure().add_subplot(projection='3d') @@ -38,3 +38,168 @@ further documented in the `.mplot3d.axes3d.Axes3D.view_init` API. .. plot:: gallery/mplot3d/view_planes_3d.py :align: center + + +.. _toolkit_mouse-rotation: + +Rotation with mouse +=================== + +3D plots can be reoriented by dragging the mouse. +There are various ways to accomplish this; the style of mouse rotation +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 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: +the plot reacts differently to mouse movement, dependent on the particular +orientation at hand. Also, 'roll' cannot be controlled. + +As an alternative, there are various mouse rotation styles where the mouse +manipulates a virtual 'trackball'. In its simplest form (``mouserotationstyle: trackball``), +the trackball rotates around an in-plane axis perpendicular to the mouse motion +(it is as if there is a plate laying on the trackball; the plate itself is fixed +in orientation, but you can drag the plate with the mouse, thus rotating the ball). +This is more natural to work with than the ``azel`` style; however, +the plot cannot be easily rotated around the viewing direction - one has to +move the mouse in circles with a handedness opposite to the desired rotation, +counterintuitively. + +A different variety of trackball rotates along the shortest arc on the virtual +sphere (``mouserotationstyle: sphere``). Rotating around the viewing direction +is straightforward with it: grab the ball near its edge instead of near the center. + +Ken Shoemake's ARCBALL [Shoemake1992]_ is also available (``mouserotationstyle: Shoemake``); +it resembles the ``sphere`` style, but is free of hysteresis, +i.e., returning mouse to the original position +returns the figure to its original orientation; the rotation is independent +of the details of the path the mouse took, which could be desirable. +However, Shoemake's arcball rotates at twice the angular rate of the +mouse movement (it is quite noticeable, especially when adjusting roll), +and it lacks an obvious mechanical equivalent; arguably, the path-independent +rotation is not natural (however convenient), it could take some getting used to. +So it is a trade-off. + +Henriksen et al. [Henriksen2002]_ provide an overview. In summary: + +.. list-table:: + :width: 100% + :widths: 30 20 20 20 20 35 + + * - Style + - traditional [1]_ + - incl. roll [2]_ + - uniform [3]_ + - path independent [4]_ + - mechanical counterpart [5]_ + * - azel + - ✔️ + - ❌ + - ❌ + - ✔️ + - ✔️ + * - trackball + - ❌ + - ✓ [6]_ + - ✔️ + - ❌ + - ✔️ + * - sphere + - ❌ + - ✔️ + - ✔️ + - ❌ + - ✔️ + * - arcball + - ❌ + - ✔️ + - ✔️ + - ✔️ + - ❌ + + +.. [1] The way it was prior to v3.10; this is also MATLAB's style +.. [2] Mouse controls roll too (not only azimuth and elevation) +.. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator') +.. [4] Returning mouse to original position returns figure to original orientation (rotation is independent of the details of the path the mouse took) +.. [5] The style has a corresponding natural implementation as a mechanical device +.. [6] While it is possible to control roll with the ``trackball`` style, this is not immediately obvious (it requires moving the mouse in large circles) and a bit counterintuitive (the resulting roll is in the opposite direction) + +You can try out one of the various mouse rotation styles using: + +.. code:: + + import matplotlib as mpl + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball' + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib import cm + + ax = plt.figure().add_subplot(projection='3d') + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + + surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm, + linewidth=0, antialiased=False) + + plt.show() + +Alternatively, create a file ``matplotlibrc``, with contents:: + + axes3d.mouserotationstyle: trackball + +(or any of the other styles, instead of ``trackball``), and then run any of +the :ref:`mplot3d-examples-index` examples. + +The size of the virtual trackball, sphere, or arcball can be adjusted +by setting :rc:`axes3d.trackballsize`. This specifies how much +mouse motion is needed to obtain a given rotation angle (when near the center), +and it controls where the edge of the sphere or arcball is (how far from +the center, hence how close to the plot edge). +The size is specified in units of the Axes bounding box, +i.e., to make the arcball span the whole bounding box, set it to 1. +A size of about 2/3 appears to work reasonably well; this is the default. + +Both arcballs (``mouserotationstyle: sphere`` and +``mouserotationstyle: arcball``) have a noticeable edge; the edge can be made +less abrupt by specifying a border width, :rc:`axes3d.trackballborder`. +This works somewhat like Gavin Bell's arcball, which was +originally written for OpenGL [Bell1988]_, and is used in Blender and Meshlab. +Bell's arcball extends the arcball's spherical control surface with a hyperbola; +the two are smoothly joined. However, the hyperbola extends all the way beyond +the edge of the plot. In the mplot3d sphere and arcball style, the border extends +to a radius ``trackballsize/2 + trackballborder``. +Beyond the border, the style works like the original: it controls roll only. +A border width of about 0.2 appears to work well; this is the default. +To obtain the original Shoemake's arcball with a sharp border, +set the border width to 0. +For an extended border similar to Bell's arcball, where the transition from +the arcball to the border occurs at 45°, set the border width to +:math:`\sqrt 2 \approx 1.414`. +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. + + +.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying + three-dimensional rotation using a mouse", in Proceedings of Graphics + Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 + +.. [Bell1988] Gavin Bell, in the examples included with the GLUT (OpenGL + Utility Toolkit) library, + https://github.com/markkilgard/glut/blob/master/progs/examples/trackball.h + +.. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk, + "Virtual Trackballs Revisited", in IEEE Transactions on Visualization + and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216, + https://doi.org/10.1109/TVCG.2004.1260772 `[full-text]`__; + +__ https://www.researchgate.net/publication/8329656_Virtual_Trackballs_Revisited#fullTextFileContent diff --git a/doc/api/transforms.dot b/doc/api/transforms.dot new file mode 100644 index 000000000000..c3ea975158bf --- /dev/null +++ b/doc/api/transforms.dot @@ -0,0 +1,141 @@ +digraph { + splines="polyline"; + + node [ + fontname="DejaVu Sans, Vera Sans, Liberation Sans, Arial, Helvetica, sans", + shape=box, + ]; + edge [ + arrowsize=0.5, + fontname="DejaVu Sans, Vera Sans, Liberation Sans, Arial, Helvetica, sans", + ]; + + // Axes properties. + Axes__bbox [ + label=Axes.bbox>, + target="_top", + tooltip="TransformedBbox", + URL="transformations.html#matplotlib.transforms.TransformedBbox", + ]; + Axes__transAxes [ + label=Axes.transAxes> + target="_top", + tooltip="BboxTransformTo", + URL="transformations.html#matplotlib.transforms.BboxTransformTo", + ]; + Axes__transData [ + label=Axes.transData> + target="_top", + tooltip="CompositeGenericTransform", + URL="transformations.html#matplotlib.transforms.CompositeGenericTransform", + ]; + Axes__transLimits [ + label=Axes.transLimits> + target="_top", + tooltip="BboxTransformFrom", + URL="transformations.html#matplotlib.transforms.BboxTransformFrom", + ]; + Axes__transScale [ + label=Axes.transScale> + target="_top", + tooltip="TransformWrapper", + URL="transformations.html#matplotlib.transforms.TransformWrapper", + ]; + Axes__position [ + label=Axes.get_position()> + target="_top", + tooltip="Bbox", + URL="transformations.html#matplotlib.transforms.Bbox", + ]; + Axes__viewLim [ + label = Axes._viewLim> + target="_top", + tooltip="Bbox", + URL="transformations.html#matplotlib.transforms.Bbox", + ]; + + // Axis properties. + XAxis_transform [ + label=Axes.xaxis.get_transform()> + target="_top", + tooltip="IdentityTransform", + URL="transformations.html#matplotlib.transforms.IdentityTransform", + ]; + YAxis_transform [ + label=Axes.yaxis.get_transform()> + target="_top", + tooltip="IdentityTransform", + URL="transformations.html#matplotlib.transforms.IdentityTransform", + ]; + + // Figure properties. + Figure__transFigure [ + label=Figure.transFigure> + target="_top", + tooltip="BboxTransformTo", + URL="transformations.html#matplotlib.transforms.BboxTransformTo", + ]; + Figure__bbox [ + label=Figure.bbox> + target="_top", + tooltip="TransformedBbox", + URL="transformations.html#matplotlib.transforms.TransformedBbox", + ]; + Figure__bbox_inches [ + label=Figure.bbox_inches> + target="_top", + tooltip="Bbox", + URL="transformations.html#matplotlib.transforms.Bbox", + ]; + Figure__dpi_scale_trans [ + label=Figure.dpi_scale_trans> + target="_top", + tooltip="Affine2D", + URL="transformations.html#matplotlib.transforms.Affine2D", + ]; + + // Internal unnamed transform children. + Axes__transDataB [ + label="CompositeGenericTransform", + target="_top", + tooltip="CompositeGenericTransform", + URL="transformations.html#matplotlib.transforms.CompositeGenericTransform", + ]; + Axes__transLimitsBbox [ + label="TransformedBbox", + target="_top", + tooltip="TransformedBbox", + URL="transformations.html#matplotlib.transforms.TransformedBbox", + ]; + Axes__transScaleBlend [ + label="BlendedAffine2D", + target="_top", + tooltip="BlendedAffine2D", + URL="transformations.html#matplotlib.transforms.BlendedAffine2D", + ]; + + // The actual Axes__transform tree follows: + Axes__transData -> Axes__transScale [label="a", labelangle=90]; + Axes__transData -> Axes__transDataB [label="b"]; + Axes__transDataB -> Axes__transLimits [label="a"]; + Axes__transDataB -> Axes__transAxes [label="b"]; + + Axes__transScale -> Axes__transScaleBlend [label="child"]; + Axes__transScaleBlend -> XAxis_transform [label="x_transform"]; + Axes__transScaleBlend -> YAxis_transform [label="y_transform"]; + + Axes__transLimits -> Axes__transLimitsBbox [label="boxin"]; + Axes__transLimitsBbox -> Axes__viewLim [label="bbox"]; + Axes__transLimitsBbox -> Axes__transScale [label="transform"]; + + Axes__transAxes -> Axes__bbox [label="boxout"]; + Axes__bbox -> Axes__position [label="bbox"]; + Axes__bbox -> Figure__transFigure [label="transform"]; + + Figure__transFigure -> Figure__bbox [label="boxout"]; + Figure__bbox -> Figure__bbox_inches [label="bbox"]; + Figure__bbox -> Figure__dpi_scale_trans [label="transform"]; +} diff --git a/doc/conf.py b/doc/conf.py index 04f48204eb77..96738492b68b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,6 +15,7 @@ import logging import os from pathlib import Path +import re import shutil import subprocess import sys @@ -28,7 +29,6 @@ import matplotlib - # debug that building expected version print(f"Building Documentation for Matplotlib: {matplotlib.__version__}") @@ -107,6 +107,7 @@ def _parse_skip_subdirs_file(): extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', + 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', @@ -206,8 +207,20 @@ def _check_dependencies(): warnings.filterwarnings('ignore', category=UserWarning, message=r'(\n|.)*is non-interactive, and thus cannot be shown') + +# hack to catch sphinx-gallery 17.0 warnings +def tutorials_download_error(record): + if re.match("download file not readable: .*tutorials_(python|jupyter).zip", + record.msg): + return False + + +logger = logging.getLogger('sphinx') +logger.addFilter(tutorials_download_error) + autosummary_generate = True autodoc_typehints = "none" +autodoc_mock_imports = ["pytest"] # we should ignore warnings coming from importing deprecated modules for # autodoc purposes, as this will disappear automatically when they are removed @@ -218,6 +231,20 @@ def _check_dependencies(): autodoc_docstring_signature = True autodoc_default_options = {'members': None, 'undoc-members': None} + +def autodoc_process_bases(app, name, obj, options, bases): + """ + Hide pybind11 base object from inheritance tree. + + Note, *bases* must be modified in place. + """ + for cls in bases[:]: + if not isinstance(cls, type): + continue + if cls.__module__ == 'pybind11_builtins' and cls.__name__ == 'pybind11_object': + bases.remove(cls) + + # make sure to ignore warnings that stem from simply inspecting deprecated # class-level attributes warnings.filterwarnings('ignore', category=DeprecationWarning, @@ -242,6 +269,7 @@ def _check_dependencies(): 'tornado': ('https://www.tornadoweb.org/en/stable/', None), 'xarray': ('https://docs.xarray.dev/en/stable/', None), 'meson-python': ('https://mesonbuild.com/meson-python/', None), + 'pip': ('https://pip.pypa.io/en/stable/', None), } @@ -279,6 +307,11 @@ def _check_dependencies(): 'copyfile_regex': r'.*\.rst', } +if parse_version(sphinx_gallery.__version__) >= parse_version('0.17.0'): + sphinx_gallery_conf['parallel'] = True + # Any warnings from joblib turned into errors may cause a deadlock. + warnings.filterwarnings('default', category=UserWarning, module='joblib') + if 'plot_gallery=0' in sys.argv: # Gallery images are not created. Suppress warnings triggered where other # parts of the documentation link to these images. @@ -348,8 +381,8 @@ def gallery_image_warning_filter(record): # This is the default encoding, but it doesn't hurt to be explicit source_encoding = "utf-8" -# The toplevel toctree document (renamed to root_doc in Sphinx 4.0) -root_doc = master_doc = 'index' +# The toplevel toctree document. +root_doc = 'index' # General substitutions. try: @@ -819,6 +852,58 @@ def linkcode_resolve(domain, info): extensions.append('sphinx.ext.viewcode') +def generate_ScalarMappable_docs(): + + import matplotlib.colorizer + from numpydoc.docscrape_sphinx import get_doc_object + from pathlib import Path + import textwrap + from sphinx.util.inspect import stringify_signature + target_file = Path(__file__).parent / 'api' / 'scalarmappable.gen_rst' + with open(target_file, 'w') as fout: + fout.write(""" +.. class:: ScalarMappable(colorizer, **kwargs) + :canonical: matplotlib.colorizer._ScalarMappable + +""") + for meth in [ + matplotlib.colorizer._ScalarMappable.autoscale, + matplotlib.colorizer._ScalarMappable.autoscale_None, + matplotlib.colorizer._ScalarMappable.changed, + """ + .. attribute:: colorbar + + The last colorbar associated with this ScalarMappable. May be None. +""", + matplotlib.colorizer._ScalarMappable.get_alpha, + matplotlib.colorizer._ScalarMappable.get_array, + matplotlib.colorizer._ScalarMappable.get_clim, + matplotlib.colorizer._ScalarMappable.get_cmap, + """ + .. property:: norm +""", + matplotlib.colorizer._ScalarMappable.set_array, + matplotlib.colorizer._ScalarMappable.set_clim, + matplotlib.colorizer._ScalarMappable.set_cmap, + matplotlib.colorizer._ScalarMappable.set_norm, + matplotlib.colorizer._ScalarMappable.to_rgba, + ]: + if isinstance(meth, str): + fout.write(meth) + else: + name = meth.__name__ + sig = stringify_signature(inspect.signature(meth)) + docstring = textwrap.indent( + str(get_doc_object(meth)), + ' ' + ).rstrip() + fout.write(f""" + .. method:: {name}{sig} +{docstring} + +""") + + # ----------------------------------------------------------------------------- # Sphinx setup # ----------------------------------------------------------------------------- @@ -829,5 +914,7 @@ def setup(app): bld_type = 'rel' app.add_config_value('skip_sub_dirs', 0, '') app.add_config_value('releaselevel', bld_type, 'env') + app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) + generate_ScalarMappable_docs() diff --git a/doc/devel/MEP/MEP23.rst b/doc/devel/MEP/MEP23.rst index d6b342877959..ec56f362c867 100644 --- a/doc/devel/MEP/MEP23.rst +++ b/doc/devel/MEP/MEP23.rst @@ -38,8 +38,8 @@ desirable to be able to group these under the same window. See :ghpull:`2194`. The proposed solution modifies `.FigureManagerBase` to contain and manage more -than one ``Canvas``. The settings parameter :rc:`backend.multifigure` control -when the **MultiFigure** behaviour is desired. +than one ``Canvas``. The ``backend.multifigure`` rcParam controls when the +**MultiFigure** behaviour is desired. **Note** diff --git a/doc/devel/api_changes.rst b/doc/devel/api_changes.rst index b7d0a4b063ce..6e134d6b9509 100644 --- a/doc/devel/api_changes.rst +++ b/doc/devel/api_changes.rst @@ -9,13 +9,8 @@ if the added benefit is worth the effort of adapting existing code. Because we are a visualization library, our primary output is the final visualization the user sees; therefore, the appearance of the figure is part of -the API and any changes, either semantic or :ref:`aesthetic `, -are backwards-incompatible API changes. - -.. toctree:: - :hidden: - - color_changes.rst +the API and any changes, either semantic or aesthetic, are backwards-incompatible +API changes. Add new API and features @@ -37,6 +32,23 @@ take particular care when adding new API: __ https://emptysqua.re/blog/api-evolution-the-right-way/#adding-parameters +Add or change colormaps, color sequences, and styles +---------------------------------------------------- +Visual changes are considered an API break. Therefore, we generally do not modify +existing colormaps, color sequences, or styles. + +We put a high bar on adding new colormaps and styles to prevent excessively growing +them. While the decision is case-by-case, evaluation criteria include: + +- novelty: Does it support a new use case? e.g. slight variations of existing maps, + sequences and styles are likely not accepted. +- usability and accessibility: Are colors of sequences sufficiently distinct? Has + colorblindness been considered? +- evidence of wide spread usage: for example academic papers, industry blogs and + whitepapers, or inclusion in other visualization libraries or domain specific tools +- open license: colormaps, sequences, and styles must have a BSD compatible license + (see :ref:`license-discussion`) + .. _deprecation-guidelines: Deprecate API @@ -216,8 +228,8 @@ API change notes """""""""""""""" .. include:: ../api/next_api_changes/README.rst - :start-line: 5 - :end-line: 31 + :start-after: api-change-guide-start + :end-before: api-change-guide-end .. _whats-new-notes: @@ -225,5 +237,5 @@ What's new notes """""""""""""""" .. include:: ../users/next_whats_new/README.rst - :start-line: 5 - :end-line: 24 + :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 77247ba9a3b2..36802de49bd0 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -89,8 +89,12 @@ We generally use `stub files the type information for ``colors.py``. A notable exception is ``pyplot.py``, which is type hinted inline. -Type hints are checked by the mypy :ref:`pre-commit hook `, -can often be verified by running ``tox -e stubtest``. +Type hints can be validated by the `stubtest +`_ tool, which can be run +locally using ``tox -e stubtest`` and is a part of the :ref:`automated-tests` +suite. Type hints for existing functions are also checked by the mypy +:ref:`pre-commit hook `. + New modules and files: installation =================================== diff --git a/doc/devel/color_changes.rst b/doc/devel/color_changes.rst deleted file mode 100644 index f7646ded7c14..000000000000 --- a/doc/devel/color_changes.rst +++ /dev/null @@ -1,136 +0,0 @@ -.. _color_changes: - -********************* -Default color changes -********************* - -As discussed at length `elsewhere `__ , -``jet`` is an -empirically bad colormap and should not be the default colormap. -Due to the position that changing the appearance of the plot breaks -backward compatibility, this change has been put off for far longer -than it should have been. In addition to changing the default color -map we plan to take the chance to change the default color-cycle on -plots and to adopt a different colormap for filled plots (``imshow``, -``pcolor``, ``contourf``, etc) and for scatter like plots. - - -Default heat map colormap -------------------------- - -The choice of a new colormap is fertile ground to bike-shedding ("No, -it should be _this_ color") so we have a proposed set criteria (via -Nathaniel Smith) to evaluate proposed colormaps. - -- it should be a sequential colormap, because diverging colormaps are - really misleading unless you know where the "center" of the data is, - and for a default colormap we generally won't. - -- it should be perceptually uniform, i.e., human subjective judgments - of how far apart nearby colors are should correspond as linearly as - possible to the difference between the numerical values they - represent, at least locally. - -- it should have a perceptually uniform luminance ramp, i.e. if you - convert to greyscale it should still be uniform. This is useful both - in practical terms (greyscale printers are still a thing!) and - because luminance is a very strong and natural cue to magnitude. - -- it should also have some kind of variation in hue, because hue - variation is a really helpful additional cue to perception, having - two cues is better than one, and there's no reason not to do it. - -- the hue variation should be chosen to produce reasonable results - even for viewers with the more common types of - colorblindness. (Which rules out things like red-to-green.) - -- For bonus points, it would be nice to choose a hue ramp that still - works if you throw away the luminance variation, because then we - could use the version with varying luminance for 2d plots, and the - version with just hue variation for 3d plots. (In 3d plots you - really want to reserve the luminance channel for lighting/shading, - because your brain is *really* good at extracting 3d shape from - luminance variation. If the 3d surface itself has massively varying - luminance then this screws up the ability to see shape.) - -- Not infringe any existing IP - -Example script -++++++++++++++ - -Proposed colormaps -++++++++++++++++++ - -Default scatter colormap ------------------------- - -For heat-map like applications it can be desirable to cover as much of -the luminance scale as possible, however when colormapping markers, -having markers too close to white can be a problem. For that reason -we propose using a different (but maybe related) colormap to the -heat map for marker-based. The design parameters are the same as -above, only with a more limited luminance variation. - - -Example script -++++++++++++++ -:: - - import numpy as np - import matplotlib.pyplot as plt - - np.random.seed(1234) - - fig, (ax1, ax2) = plt.subplots(1, 2) - - N = 50 - x = np.random.rand(N) - y = np.random.rand(N) - colors = np.random.rand(N) - area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radiuses - - ax1.scatter(x, y, s=area, c=colors, alpha=0.5) - - - X,Y = np.meshgrid(np.arange(0, 2*np.pi, .2), - np.arange(0, 2*np.pi, .2)) - U = np.cos(X) - V = np.sin(Y) - Q = ax2.quiver(X, Y, U, V, units='width') - qd = np.random.rand(np.prod(X.shape)) - Q.set_array(qd) - -Proposed colormaps -++++++++++++++++++ - -Color cycle / qualitative colormap ------------------------------------ - -When plotting lines it is frequently desirable to plot multiple lines -or artists which need to be distinguishable, but there is no inherent -ordering. - - -Example script -++++++++++++++ -:: - - import numpy as np - import matplotlib.pyplot as plt - - fig, (ax1, ax2) = plt.subplots(1, 2) - - x = np.linspace(0, 1, 10) - - for j in range(10): - ax1.plot(x, x * j) - - - th = np.linspace(0, 2*np.pi, 1024) - for j in np.linspace(0, np.pi, 10): - ax2.plot(th, np.sin(th + j)) - - ax2.set_xlim(0, 2*np.pi) - -Proposed color cycle -++++++++++++++++++++ diff --git a/doc/devel/communication_guide.rst b/doc/devel/communication_guide.rst index aed52be84e32..04c5dae93bdc 100644 --- a/doc/devel/communication_guide.rst +++ b/doc/devel/communication_guide.rst @@ -21,7 +21,7 @@ Our approach to community engagement is foremost guided by our :ref:`mission-sta who may no longer be active on GitHub, build relationships with potential contributors, and connect with other projects and communities who use Matplotlib. -* In prioritizing understandability and extensiblity, we recognize that people +* In prioritizing understandability and extensibility, we recognize that people using Matplotlib, in whatever capacity, are part of our community. Doing so empowers our community members to build community with each other, for example by creating educational resources, building third party tools, and building diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 7b2b0e774ec7..0e7a9ef5115e 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -2,10 +2,9 @@ .. _contributing: -********** -Contribute -********** - +****************** +Contributing guide +****************** You've discovered a bug or something else you want to change in Matplotlib — excellent! @@ -13,23 +12,11 @@ You've worked out a way to fix it — even better! You want to tell us about it — best of all! -This project is a community effort, and everyone is welcome to contribute. Everyone -within the community is expected to abide by our `code of conduct -`_. - Below, you can find a number of ways to contribute, and how to connect with the Matplotlib community. -.. _start-contributing: - -Get started -=========== - -There is no pre-defined pathway for new contributors -- we recommend looking at -existing issue and pull request discussions, and following the conversations -during pull request reviews to get context. Or you can deep-dive into a subset -of the code-base to understand what is going on. - +Ways to contribute +================== .. dropdown:: Do I really have something to contribute to Matplotlib? :open: :icon: person-fill @@ -109,7 +96,7 @@ Code is contributed through pull requests, so we recommend that you start at Documentation ------------- -You as an end-user of Matplotlib can make a valuable contribution because you +You, as an end-user of Matplotlib can make a valuable contribution because you can more clearly see the potential for improvement than a core developer. For example, you can: @@ -161,6 +148,24 @@ please reach out on the :ref:`contributor_incubator` .. _`open an issue`: https://github.com/matplotlib/matplotlib/issues/new?assignees=&labels=Documentation&projects=&template=documentation.yml&title=%5BDoc%5D%3A+ +.. _contribute_triage: + +Triage +------ +We appreciate your help keeping the `issue tracker `_ +organized because it is our centralized location for feature requests, +bug reports, tracking major projects, and discussing priorities. Some examples of what +we mean by triage are: + +* labeling issues and pull requests +* verifying bug reports +* debugging and resolving issues +* linking to related issues, discussion, and external work + +Our triage process is discussed in detail in :ref:`bug_triaging`. + +If you have any questions about the process, please reach out on the +:ref:`contributor_incubator` .. _other_ways_to_contribute: @@ -179,38 +184,33 @@ If you have developed an extension to Matplotlib, please consider adding it to o `third party package `_ list. -.. _get_connected: +.. _generative_ai: -Get connected -============= -When in doubt, we recommend going together! Get connected with our community of -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. -.. _contributor_incubator: +Restrictions on Generative AI Usage +=================================== -Contributor incubator ---------------------- +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. -The incubator is our non-public communication channel for new contributors. It -is a private gitter_ (chat) room moderated by core Matplotlib developers where -you can get guidance and support for your first few PRs. It's a place where you -can ask questions about anything: how to use git, GitHub, how our PR review -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 -``#incubator``. One of our core developers will see your message and will add you. +.. _new_contributors: -.. _gitter: https://gitter.im/matplotlib/matplotlib -.. _community: https://gitter.im/matplotlib/community +New contributors +================ +There is no pre-defined pathway for new contributors - we recommend looking at +existing issue and pull request discussions, and following the conversations +during pull request reviews to get context. Or you can deep-dive into a subset +of the code-base to understand what is going on. -.. _new_contributors: +.. _new_contributors_meeting: -New Contributors Meeting +New contributors meeting ------------------------ Once a month, we host a meeting to discuss topics that interest new @@ -226,20 +226,24 @@ questions you might have, and to get to know a few of the people behind the GitHub handles 😉. You can reach out to us on gitter_ for any clarifications or suggestions. We ❤ feedback! -.. _managing_issues_prs: +.. _contributor_incubator: -Work on an issue -================ +Contributor incubator +--------------------- -In general, the Matplotlib project does not assign issues. Issues are -"assigned" or "claimed" by opening a PR; there is no other assignment -mechanism. If you have opened such a PR, please comment on the issue thread to -avoid duplication of work. Please check if there is an existing PR for the -issue you are addressing. If there is, try to work with the author by -submitting reviews of their code or commenting on the PR rather than opening -a new PR; duplicate PRs are subject to being closed. However, if the existing -PR is an outline, unlikely to work, or stalled, and the original author is -unresponsive, feel free to open a new PR referencing the old one. +The incubator is our non-public communication channel for new contributors. It +is a private gitter_ (chat) room moderated by core Matplotlib developers where +you can get guidance and support for your first few PRs. It's a place where you +can ask questions about anything: how to use git, GitHub, how our PR review +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 +``#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 .. _good_first_issues: @@ -264,122 +268,85 @@ though not necessarily all at the same time: - It involves Python features such as decorators and context managers, which have subtleties due to our implementation decisions. +.. _first_contribution: -.. _how-to-pull-request: - -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). You can do this by cloning a copy of the -Maplotlib repository to your own computer, or alternatively using -`GitHub Codespaces `_, a cloud-based -in-browser development environment that comes with the appropriated setup to -contribute to Matplotlib. - -Workflow overview ------------------ - -A brief overview of the workflow is as follows. - -#. `Create an account `_ on GitHub if you do not - already have one. - -#. Fork the `project repository `_ by - clicking on the :octicon:`repo-forked` **Fork** button near the top of the page. - This creates a copy of the code under your account on the GitHub server. - -#. Set up a development environment: - - .. tab-set:: - - .. tab-item:: Local development - - Clone this copy to your local disk:: - - git clone https://github.com//matplotlib.git - - .. tab-item:: Using GitHub Codespaces - - Check out the Matplotlib repository and activate your development environment: +First contributions +------------------- - #. Open codespaces on your fork by clicking on the green "Code" button - on the GitHub web interface and selecting the "Codespaces" tab. +If this is your first open source contribution, or your first time contributing to Matplotlib, +and you need help or guidance finding a good first issue, look no further. This section will +guide you through each step: - #. Next, click on "Open codespaces on ". You will be - able to change branches later, so you can select the default - ``main`` branch. +1. Navigate to the `issues page `_. +2. Filter labels with `"Difficulty: Easy" `_ + & `"Good first Issue" `_ (optional). +3. Click on an issue you would like to work on, and check to see if the issue has a pull request opened to resolve it. - #. After the codespace is created, you will be taken to a new browser - tab where you can use the terminal to activate a pre-defined conda - environment called ``mpl-dev``:: + * A good way to judge if you chose a suitable issue is by asking yourself, "Can I independently submit a PR in 1-2 weeks?" +4. Check existing pull requests (e.g., :ghpull:`28476`) and filter by the issue number to make sure the issue is not in progress: - conda activate mpl-dev + * 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 `_. +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 `_. +.. dropdown:: `Pull request template `_ + :open: -#. Install the local version of Matplotlib with:: - - python -m pip install --no-build-isolation --editable .[dev] - - See :ref:`installing_for_devs` for detailed instructions. - -#. Create a branch to hold your changes:: - - git checkout -b my-feature origin/main + .. literalinclude:: ../../.github/PULL_REQUEST_TEMPLATE.md + :language: markdown - and start making changes. Never work in the ``main`` branch! +.. _get_connected: -#. Work on this task using Git to do the version control. Codespaces persist for - some time (check the `documentation for details - `_) - and can be managed on https://github.com/codespaces. When you're done editing - e.g., ``lib/matplotlib/collections.py``, do:: +Get connected +============= - git add lib/matplotlib/collections.py - git commit +When in doubt, we recommend going together! Get connected with our community of +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_. - to record your changes in Git, then push them to your GitHub fork with:: +.. _gitter: https://gitter.im/matplotlib/matplotlib +.. _community: https://gitter.im/matplotlib/community - git push -u origin my-feature +.. _managing_issues_prs: -GitHub Codespaces workflows -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Choose an issue +=============== -If you need to open a GUI window with Matplotlib output on Codespaces, our -configuration includes a `light-weight Fluxbox-based desktop -`_. -You can use it by connecting to this desktop via your web browser. To do this: +In general, the Matplotlib project does not assign issues. Issues are +"assigned" or "claimed" by opening a PR; there is no other assignment +mechanism. If you have opened such a PR, please comment on the issue thread to +avoid duplication of work. Please check if there is an existing PR for the +issue you are addressing. If there is, try to work with the author by +submitting reviews of their code or commenting on the PR rather than opening +a new PR; duplicate PRs are subject to being closed. However, if the existing +PR is an outline, unlikely to work, or stalled, and the original author is +unresponsive, feel free to open a new PR referencing the old one. -#. Press ``F1`` or ``Ctrl/Cmd+Shift+P`` and select - ``Ports: Focus on Ports View`` in the VSCode session to bring it into - focus. Open the ports view in your tool, select the ``noVNC`` port, and - click the Globe icon. -#. In the browser that appears, click the Connect button and enter the desktop - password (``vscode`` by default). +.. _how-to-pull-request: -Check the `GitHub instructions -`_ -for more details on connecting to the desktop. +Start a pull request +==================== -View documentation -"""""""""""""""""" +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: -If you also built the documentation pages, you can view them using Codespaces. -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." +#. **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 + following the instructions in :ref:`installing_for_devs` +#. **Then** start a pull request by following the guidance in :ref:`development workflow ` -Open a pull request on Matplotlib ---------------------------------- +#. **After starting** check that your contribution meets the :ref:`pull request guidelines ` + and :ref:`update the pull request ` as needed. -Finally, go to the web page of *your fork* of the Matplotlib repo, and click -**Compare & pull request** to send your changes to the maintainers for review. -The base repository is ``matplotlib/matplotlib`` and the base branch is -generally ``main``. For more guidance, see GitHub's `pull request tutorial -`_. +#. **Finally** follow up with maintainers on the PR if waiting more than a few days for + feedback. -For more detailed instructions on how to set up Matplotlib for development and -best practices for contribution, see :ref:`installing_for_devs` and -:ref:`development-workflow`. +If you have questions of any sort, reach out on the :ref:`contributor_incubator` and join +the :ref:`new_contributors_meeting`. diff --git a/doc/devel/development_setup.rst b/doc/devel/development_setup.rst index be99bed2fe5f..cffda17939a7 100644 --- a/doc/devel/development_setup.rst +++ b/doc/devel/development_setup.rst @@ -28,11 +28,11 @@ Fork the Matplotlib repository Matplotlib is hosted at https://github.com/matplotlib/matplotlib.git. If you plan on solving issues or submitting pull requests to the main Matplotlib -repository, you should first *fork* this repository by visiting -https://github.com/matplotlib/matplotlib.git and clicking on the -``Fork`` :octicon:`repo-forked` button on the top right of the page. See -`the GitHub documentation `__ -for more details. +repository, you should first fork this repository by *clicking* the +:octicon:`repo-forked` **Fork** button near the top of the `project repository `_ page. + +This creates a copy of the code under your account on the GitHub server. See `the GitHub +documentation `__ for more details. Retrieve the latest version of the code ======================================= @@ -111,8 +111,9 @@ Create a dedicated environment You should set up a dedicated environment to decouple your Matplotlib development from other Python and Matplotlib installations on your system. -The simplest way to do this is to use either Python's virtual environment -`venv`_ or `conda`_. +We recommend using one of the following options for a dedicated development environment +because these options are configured to install the Python dependencies as part of their +setup. .. _venv: https://docs.python.org/3/library/venv.html .. _conda: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html @@ -138,6 +139,8 @@ The simplest way to do this is to use either Python's virtual environment pip install -r requirements/dev/dev-requirements.txt + Remember to activate the environment whenever you start working on Matplotlib! + .. tab-item:: conda environment Create a new `conda`_ environment and install the Python dependencies with :: @@ -153,21 +156,67 @@ The simplest way to do this is to use either Python's virtual environment conda activate mpl-dev -Remember to activate the environment whenever you start working on Matplotlib. + Remember to activate the environment whenever you start working on Matplotlib! + + .. tab-item:: :octicon:`codespaces` GitHub Codespaces + + `GitHub Codespaces `_ is a cloud-based + in-browser development environment that comes with the appropriate setup to + contribute to Matplotlib. + + #. Open codespaces on your fork by clicking on the green :octicon:`code` ``Code`` + button on the GitHub web interface and selecting the ``Codespaces`` tab. + + #. Next, click on "Open codespaces on ". You will be + able to change branches later, so you can select the default + ``main`` branch. + + #. After the codespace is created, you will be taken to a new browser + tab where you can use the terminal to activate a pre-defined conda + environment called ``mpl-dev``:: + + conda activate mpl-dev + + Remember to activate the *mpl-dev* environment whenever you start working on + Matplotlib. -Install Dependencies -==================== + If you need to open a GUI window with Matplotlib output on Codespaces, our + configuration includes a `light-weight Fluxbox-based desktop + `_. + You can use it by connecting to this desktop via your web browser. To do this: -Most Python dependencies will be installed when :ref:`setting up the environment ` -but non-Python dependencies like C++ compilers, LaTeX, and other system applications -must be installed separately. + #. Press ``F1`` or ``Ctrl/Cmd+Shift+P`` and select + ``Ports: Focus on Ports View`` in the VSCode session to bring it into + focus. Open the ports view in your tool, select the ``noVNC`` port, and + click the Globe icon. + #. In the browser that appears, click the Connect button and enter the desktop + password (``vscode`` by default). -.. toctree:: - :maxdepth: 2 + Check the `GitHub instructions + `_ + for more details on connecting to the desktop. - ../install/dependencies + If you also built the documentation pages, you can view them using Codespaces. + 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." +Install external dependencies +============================= + +Python dependencies were installed as part of :ref:`setting up the environment `. +Additionally, the following non-Python dependencies must also be installed locally: + +.. rst-class:: checklist + +* :ref:`c++ compiler` +* :ref:`documentation build dependencies ` + + +For a full list of dependencies, see :ref:`dependencies`. External dependencies do not +need to be installed when working in codespaces. + .. _development-install: Install Matplotlib in editable mode @@ -254,3 +303,9 @@ listed in ``.pre-commit-config.yaml``, against the full codebase with :: To run a particular hook manually, run ``pre-commit run`` with the hook id :: pre-commit run --all-files + + +Please note that the ``mypy`` pre-commit hook cannot check the :ref:`type-hints` +for new functions; instead the stubs for new functions are checked using the +``stubtest`` :ref:`CI check ` and can be checked locally using +``tox -e stubtest``. diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index 23e2c17732e2..438b93314171 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -28,6 +28,30 @@ why you did it, we recommend the following: Matplotlib developers can give feedback and eventually include your suggested code into the ``main`` branch. +Overview +-------- + +After :ref:`setting up a development environment `, the typical +workflow is: + +#. Fetch all changes from ``upstream/main``:: + + git fetch upstream/main + +#. Start a new *feature branch* from ``upstream/main``:: + + git checkout -b my-feature upstream/main + +#. When you're done editing, e.g., ``lib/matplotlib/collections.py``, record your changes in Git:: + + git add lib/matplotlib/collections.py + git commit -m 'a commit message' + +#. Push the changes to your GitHub fork:: + + git push -u origin my-feature + + .. _update-mirror-main: Update the ``main`` branch @@ -49,16 +73,14 @@ Make a new feature branch When you are ready to make some changes to the code, you should start a new branch. Branches that are for a collection of related edits are often called -'feature branches'. - -Making a new branch for each set of related changes will make it easier for -someone reviewing your branch to see what you are doing. +'feature branches'. Making a new branch for each set of related changes will make it +easier for someone reviewing your branch to see what you are doing. Choose an informative name for the branch to remind yourself and the rest of us what the changes in the branch are for. For example ``add-ability-to-fly``, or ``bugfix-for-issue-42``. -:: +The process for creating a new feature branch is:: # Update the main branch git fetch upstream @@ -79,19 +101,6 @@ default, git will have a link to your fork of the GitHub repo, called git push origin my-new-feature -In git >= 1.7 you can ensure that the link is correctly set by using the -``--set-upstream`` option:: - - git push --set-upstream origin my-new-feature - -From now on git will know that ``my-new-feature`` is related to the -``my-new-feature`` branch in the GitHub repo. - -If you first opened the pull request from your ``main`` branch and then -converted it to a feature branch, you will need to close the original pull -request and open a new pull request from the renamed branch. See -`GitHub: working with branches -`_. .. _edit-flow: @@ -143,6 +152,11 @@ Open a pull request When you are ready to ask for someone to review your code and consider a merge, `submit your Pull Request (PR) `_. +Go to the web page of *your fork* of the Matplotlib repo, and click +``Compare & pull request`` to send your changes to the maintainers for review. +The base repository is ``matplotlib/matplotlib`` and the base branch is +generally ``main``. + 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. @@ -151,6 +165,9 @@ 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. +For more guidance on the mechanics of making a pull request, see GitHub's +`pull request tutorial `_. + .. _update-pull-request: Update a pull request @@ -167,6 +184,17 @@ You can achieve this by using git commit -a --amend --no-edit git push [your-remote-repo] [your-branch] --force-with-lease +.. tip:: + Instead of typing your branch name every time, you only need to type the following once to link the remote branch to the local branch:: + + git push --set-upstream origin my-new-feature + + From now on git will know that ``my-new-feature`` is related to the + ``my-new-feature`` branch in the GitHub repo. After this, you will be able to + push your changes with:: + + git push + Manage commit history ===================== diff --git a/doc/devel/document.rst b/doc/devel/document.rst index 620c12c8db1c..0d746fb69318 100644 --- a/doc/devel/document.rst +++ b/doc/devel/document.rst @@ -894,8 +894,6 @@ these ``*.rst`` files from the source location to the build location (see In the Python files, to exclude an example from having a plot generated, insert "sgskip" somewhere in the filename. -Format examples ---------------- The format of these files is relatively straightforward. Properly formatted comment blocks are treated as ReST_ text, the code is @@ -937,7 +935,7 @@ like: The first comment block is treated as ReST_ text. The other comment blocks render as comments in :doc:`/gallery/lines_bars_and_markers/simple_plot`. -Tutorials are made with the exact same mechanism, except they are longer, and +Tutorials are made with the exact same mechanism, except they are longer and typically have more than one comment block (i.e. :ref:`quick_start`). The first comment block can be the same as the example above. Subsequent blocks of ReST text are delimited by the line ``# %%`` : @@ -1053,6 +1051,68 @@ subdirectory, but :file:`galleries/users_explain/artists` has a mix of any ``*.rst`` files to a ``:toctree:``, either in the ``README.txt`` or in a manual ``index.rst``. +Examples guidelines +------------------- + +The gallery of examples contains visual demonstrations of matplotlib features. Gallery +examples exist so that users can scan through visual examples. Unlike tutorials or user +guides, gallery examples teach by demonstration, rather than by explanation or +instruction. + +Gallery examples should contain a very brief description of *what* is being demonstrated +and, when relevant, *how* it is achieved. Explanations should be brief, providing only +the minimal context necessary for understanding the example. Cross-link related +documentation (e.g. tutorials, user guides and API entries) and tag the example with +related concepts. + +Format +^^^^^^ + +All :ref:`examples-index` should aim to follow these guidelines: + +:Title: Describe content in a short sentence (approx. 1-6 words). Do not use *demo* as + this is implied by being an example. Avoid implied verbs such as *create*, + *make*, etc, e.g. *annotated heatmaps* is preferred to *create annotated + heatmaps*. Use the simple present tense when a verb is necessary, e.g. *Fill the + area between two curves* + +:Description: In a short paragraph (approx 1-3 sentences) describe what visualization + technique is being demonstrated and how library features are used to + execute the technique, e.g. *Set bar color and bar label entries using the + color and label parameters of ~Axes.bar* + +:Plot: Clearly demonstrate the subject and, when possible, show edge cases and different + applications. While the plot should be visually appealing, prioritize keeping the + plot uncluttered. + +:Code: Write the minimum necessary to showcase the feature that is the focus of the + example. Avoid custom styling and annotation (titles, legends, colors, etc.) + when it will not improve the clarity of the example. + + Use short comments sparingly to describe what hard to follow parts of code are + doing. When more context or explanation is required, add a text paragraph before + the code example. + +:doc:`/gallery/misc/bbox_intersect` demonstrates the point of visual examples. +This example is "messy" in that it's hard to categorize, but the gallery is the right +spot for it because it makes sense to find it by visual search + +:doc:`/gallery/images_contours_and_fields/colormap_interactive_adjustment` is an +example of a good descriptive title that briefly summarizes how the showcased +library features are used to implement the demonstrated visualization technique. + +:doc:`/gallery/lines_bars_and_markers/lines_with_ticks_demo` is an example of having a +minimal amount of code necessary to showcase the feature. The lack of extraneous code +makes it easier for the reader to map which parts of code correspond to which parts of +the plot. + +Figure size +^^^^^^^^^^^ +When customizing figure sizes, we aim to avoid downscaling in rendered HTML docs. +The current width limit (induced by *pydata-sphinx-theme*) is 720px, i.e. +``figsize=(7.2, ...)``, or 896px if the page does not have subsections and +thus does not have the "On this page" navigation on the right-hand side. + Miscellaneous ============= diff --git a/doc/devel/index.rst b/doc/devel/index.rst index 9744d757c342..4c71027c75ba 100644 --- a/doc/devel/index.rst +++ b/doc/devel/index.rst @@ -16,13 +16,27 @@ Contribute :octicon:`heart;1em;sd-text-info` Thank you for your interest in helping to improve Matplotlib! :octicon:`heart;1em;sd-text-info` -There are various ways to contribute: optimizing and refactoring code, detailing -unclear documentation and writing new examples, helping the community, reporting -and fixing bugs and requesting and implementing new features... +This project is a community effort, and everyone is welcome to contribute. Everyone +within the community is expected to abide by our :ref:`code of conduct `. + +There are various ways to contribute, such as optimizing and refactoring code, +detailing unclear documentation and writing new examples, helping the community, +reporting and fixing bugs, requesting and implementing new features... .. _submitting-a-bug-report: .. _request-a-new-feature: +GitHub issue tracker +==================== + +The `issue tracker `_ serves as the +centralized location for making feature requests, reporting bugs, identifying major +projects to work on, and discussing priorities. + +We have preloaded the issue creation page with markdown forms requesting the information +we need to triage issues and we welcome you to add any additional information or +context that may be necessary for resolving the issue: + .. grid:: 1 1 2 2 .. grid-item-card:: @@ -31,9 +45,7 @@ and fixing bugs and requesting and implementing new features... :octicon:`bug;1em;sd-text-info` **Submit a bug report** ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - We have preloaded the issue creation page with a Markdown form that you can - use to provide relevant context. Thank you for your help in keeping bug reports - complete, targeted and descriptive. + Thank you for your help in keeping bug reports targeted and descriptive. .. button-link:: https://github.com/matplotlib/matplotlib/issues/new/choose :expand: @@ -47,9 +59,7 @@ and fixing bugs and requesting and implementing new features... :octicon:`light-bulb;1em;sd-text-info` **Request a new feature** ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - We will give feedback on the feature proposal. Since - Matplotlib is an open source project with limited resources, we encourage - users to then also :ref:`participate in the implementation `. + Thank you for your help in keeping feature requests well defined and tightly scoped. .. button-link:: https://github.com/matplotlib/matplotlib/issues/new/choose :expand: @@ -57,17 +67,16 @@ and fixing bugs and requesting and implementing new features... Request a feature +Since Matplotlib is an open source project with limited resources, we encourage users +to also :ref:`participate ` in fixing bugs and implementing new +features. + +Contributing guide +================== We welcome you to get more involved with the Matplotlib project! If you are new to contributing, we recommend that you first read our -:ref:`contributing guide`. If you are contributing code or -documentation, please follow our guides for setting up and managing a -:ref:`development environment and workflow`. -For code, documentation, or triage, please follow the corresponding -:ref:`contribution guidelines `. - -New contributors -================ +:ref:`contributing guide`: .. toctree:: :hidden: @@ -75,53 +84,74 @@ New contributors contribute .. grid:: 1 1 2 2 - :class-row: sd-align-minor-center - - .. grid-item:: - :class: sd-fs-5 - - :octicon:`info;1em;sd-text-info` :ref:`Where should I start? ` - - :octicon:`question;1em;sd-text-info` :ref:`Where should I ask questions? ` - - :octicon:`git-pull-request;1em;sd-text-info` :ref:`How do I work on an issue? ` - - :octicon:`codespaces;1em;sd-text-info` :ref:`How do I start a pull request? ` + :class-row: sd-fs-5 sd-align-minor-center .. grid-item:: .. grid:: 1 :gutter: 1 - :class-row: sd-fs-5 .. grid-item-card:: :link: contribute_code :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`code;1em;sd-text-info` Contribute code .. grid-item-card:: :link: contribute_documentation :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`note;1em;sd-text-info` Write documentation + .. grid-item-card:: + :link: contribute_triage + :link-type: ref + :class-card: sd-shadow-none + :class-body: sd-text-{primary} + + :octicon:`issue-opened;1em;sd-text-info` Triage issues + .. grid-item-card:: :link: other_ways_to_contribute :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`globe;1em;sd-text-info` Build community + .. grid-item:: + + .. grid:: 1 + :gutter: 1 + + .. grid-item:: + + :octicon:`info;1em;sd-text-info` :ref:`Is this my first contribution? ` + + .. grid-item:: + :octicon:`question;1em;sd-text-info` :ref:`Where do I ask questions? ` + + .. grid-item:: + + :octicon:`git-pull-request;1em;sd-text-info` :ref:`How do I choose an issue? ` + + .. grid-item:: + + :octicon:`codespaces;1em;sd-text-info` :ref:`How do I start a pull request? ` .. _development_environment: -Development environment -======================= +Development workflow +==================== + +If you are contributing code or documentation, please follow our guide for setting up +and managing a development environment and workflow: .. grid:: 1 1 2 2 @@ -159,6 +189,11 @@ Development environment 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 +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. + .. grid:: 1 1 2 2 :class-row: sf-fs-1 :gutter: 2 diff --git a/doc/devel/min_dep_policy.rst b/doc/devel/min_dep_policy.rst index a702b5930fd8..6ff083ca6dc1 100644 --- a/doc/devel/min_dep_policy.rst +++ b/doc/devel/min_dep_policy.rst @@ -49,6 +49,17 @@ without compiled extensions We will only bump these dependencies as we need new features or the old versions no longer support our minimum NumPy or Python. +We will work around bugs in our dependencies when practical. + +IPython and Matplotlib do not formally depend on each other, however there is +practical coupling for the integration of Matplotlib's UI into IPython and +IPykernel. We will ensure this integration works with at least minor or major +versions of IPython and IPykernel released in the 24 months prior to our +planned release date. Matplotlib may or may not work with older versions and +we will not warn if used with IPython or IPykernel outside of this window. + + + Test and documentation dependencies =================================== @@ -58,8 +69,10 @@ support for old versions. However, we need to be careful to not over-run what down-stream packagers support (as most of the run the tests and build the documentation as part of the packaging process). -We will support at least minor versions of the development -dependencies released in the 12 months prior to our planned release. +We will support at least minor versions of the development dependencies +released in the 12 months prior to our planned release. Specific versions that +are known to be buggy may be excluded from support using the finest-grained +filtering that is practical. We will only bump these as needed or versions no longer support our minimum Python and NumPy. @@ -76,6 +89,20 @@ In the case of GUI frameworks for which we rely on Python bindings being available, we will also drop support for bindings so old that they don't support any Python version that we support. +Security issues in dependencies +=============================== + +Generally, we do not adjust the supported versions of dependencies based on +security vulnerabilities. We are a library not an application +and the version constraints on our dependencies indicate what will work (not +what is wise to use). Users and packagers can install newer versions of the +dependencies at their discretion and evaluation of risk and impact. In +contrast, if we were to adjust our minimum supported version it is very hard +for a user to override our judgment. + +If Matplotlib aids in exploiting the underlying vulnerability we should treat +that as a critical bug in Matplotlib. + .. _list-of-dependency-min-versions: List of dependency versions diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index 055998549e78..34cb64f975df 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -20,6 +20,8 @@ limited bandwidth. If there is no feedback within a couple of days, please ping us by posting a comment to your PR or reaching out on a :ref:`communication channel ` +.. _pr-author-guidelines: + Summary for pull request authors ================================ @@ -176,18 +178,24 @@ a corresponding branch. Merging ------- +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 +mistakes. It is permissible to merge with one approval if the change is not +fundamental and can easily be reverted at any time in the future. + +.. _approvals: https://docs.github.com/en/github/collaborating-with-pull-requests/reviewing-changes-in-pull-requests -* Documentation and examples may be merged by the first reviewer. Use +Some explicit rules following from this: + +* *Documentation and examples* may be merged with a single approval. Use the threshold "is this better than it was?" as the review criteria. -* For code changes (anything in ``src`` or ``lib``) at least two - core developers (those with commit rights) should review all pull - requests. If you are the first to review a PR and approve of the - changes use the GitHub `'approve review' - `__ - tool to mark it as such. If you are a subsequent reviewer please - approve the review and if you think no more review is needed, merge - the PR. +* Minor *infrastructure updates*, e.g. temporary pinning of broken dependencies + or small changes to the CI configuration, may be merged with a single + approval. + +* *Code changes* (anything in ``src`` or ``lib``) must have two approvals. Ensure that all API changes are documented in a file in one of the subdirectories of :file:`doc/api/next_api_changes`, and significant new @@ -205,9 +213,10 @@ Merging A core dev should only champion one PR at a time and we should try to keep the flow of championed PRs reasonable. -* Do not self merge, except for 'small' patches to un-break the CI or - when another reviewer explicitly allows it (ex, "Approve modulo CI - passing, may self merge when green"). +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: diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index 5034bde1eefc..0e0ebb98fd1d 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -16,7 +16,7 @@ Release guide Versioning Scheme ================= -Maplotlib follows the `Intended Effort Versioning (EffVer) `_ +Matplotlib follows the `Intended Effort Versioning (EffVer) `_ versioning scheme: *macro.meso.micro*. @@ -143,7 +143,8 @@ prepare this list: --project 'matplotlib/matplotlib' --links > doc/users/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). + common issue is ``*`` which is interpreted as unclosed markup). Also confirm that + ``codespell`` does not find any issues. .. note:: @@ -238,9 +239,9 @@ Update version switcher Update ``doc/_static/switcher.json``: - If a micro release, :samp:`{X}.{Y}.{Z}`, no changes are needed. -- If a macro release, :samp:`{X}.{Y}.0`, change the name of :samp:`name: {X}.{Y+1} - (dev)` and :samp:`name: {X}.{Y} (stable)` as well as adding a new version for the - previous stable (:samp:`name: {X}.{Y-1}`). +- If a meso release, :samp:`{X}.{Y}.0`, change the name of :samp:`name: {X}.{Y+1} (dev)` + and :samp:`name: {X}.{Y} (stable)` as well as adding a new version for the previous + stable (:samp:`name: {X}.{Y-1}`). Verify that docs build ---------------------- @@ -367,7 +368,8 @@ PyPI. Most builders should trigger automatically once the tag is pushed to GitHu * 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 - will be available as artifacts of the build. + 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. * The auto-tick bot should open a pull request into the `conda-forge feedstock `__. Review and merge (if you have the power to). @@ -380,8 +382,14 @@ PyPI. Most builders should trigger automatically once the tag is pushed to GitHu .. _release_upload_bin: -Make distribution and upload to PyPI -==================================== +Manually uploading to PyPI +========================== + +.. note:: + + As noted above, the GitHub Actions workflow should build and upload source tarballs + and wheels automatically. If for some reason, you need to upload these artifacts + manually, then follow the instructions in this section. Once you have collected all of the wheels (expect this to take a few hours), generate the tarball:: @@ -443,7 +451,7 @@ which will copy the built docs over. If this is a final release, link the rm stable ln -s 3.7.0 stable -You will also need to edit :file:`sitemap.xml` to include +You will also need to edit :file:`sitemap.xml` and :file:`versions.html` to include the newly released version. Now commit and push everything to GitHub :: git add * diff --git a/doc/devel/style_guide.rst b/doc/devel/style_guide.rst index 9dab7a6d99d2..e35112a65e42 100644 --- a/doc/devel/style_guide.rst +++ b/doc/devel/style_guide.rst @@ -155,14 +155,19 @@ reliability and consistency in documentation. They are not interchangeable. | | | rotational | | | | | motion." | | +------------------+--------------------------+--------------+--------------+ - | Explicit, | Explicit approach of | - Explicit | - object | - | Object Oriented | programming in | - explicit | oriented | - | Programming (OOP)| Matplotlib. | - OOP | - OO-style | + | Axes interface | Usage pattern in which | - Axes | - explicit | + | | one calls methods on | interface | interface | + | | Axes and Figure (and | - call | - object | + | | sometimes other Artist) | methods on | oriented | + | | objects to configure the | the Axes / | - OO-style | + | | plot. | Figure | - OOP | + | | | object | | +------------------+--------------------------+--------------+--------------+ - | Implicit, | Implicit approach of | - Implicit | - MATLAB like| - | ``pyplot`` | programming in Matplotlib| - implicit | - Pyplot | - | | with ``pyplot`` module. | - ``pyplot`` | - pyplot | - | | | | interface | + | pyplot interface | Usage pattern in which | - ``pyplot`` | - implicit | + | | one only calls `.pyplot` | interface | interface | + | | functions to configure | - call | - MATLAB like| + | | the plot. | ``pyplot`` | - Pyplot | + | | | functions | | +------------------+--------------------------+--------------+--------------+ .. |Figure| replace:: :class:`~matplotlib.figure.Figure` diff --git a/doc/devel/tag_glossary.rst b/doc/devel/tag_glossary.rst index 9a53de6358c8..b3d0ec2bcbda 100644 --- a/doc/devel/tag_glossary.rst +++ b/doc/devel/tag_glossary.rst @@ -1,19 +1,17 @@ -:orphan: - Tag Glossary ============ -I. API tags: what content from the API reference is in the example? -II. Structural tags: what format is the example? What context can we provide? -III. Domain tags: what discipline(s) might seek this example consistently? -IV. Internal tags: what information is helpful for maintainers or contributors? +.. contents:: + :depth: 1 + :local: + :backlinks: entry API tags: what content from the API reference is in the example? ---------------------------------------------------------------- +-----------------------------------+---------------------------------------------+ -|``tag`` | use case - if not obvious | +|``tag`` | use case | +===================================+=============================================+ |**Primary or relevant plot component** | +-----------------------------------+---------------------------------------------+ @@ -142,7 +140,9 @@ Structural tags: what format is the example? What context can we provide? Domain tags: what discipline(s) might seek this example consistently? --------------------------------------------------------------------- -It's futile to draw fences around "who owns what", and that's not the point of domain tags. Domain tags help groups of people to privately organize relevant information, and so are not displayed publicly. See below for a list of existing domain tags. If you don't see the one you're looking for and you think it should exist, consider proposing it. +It's futile to draw fences around "who owns what", and that's not the point of domain +tags. See below for a list of existing domain tags. If you don't see the one you're +looking for and you think it should exist, consider proposing it. +-------------------------------+----------------------------------------+ |``tag`` | use case | @@ -163,6 +163,14 @@ It's futile to draw fences around "who owns what", and that's not the point of d Internal tags: what information is helpful for maintainers or contributors? --------------------------------------------------------------------------- +These tags should be used only for development purposes; therefore please add them +separately behind a version guard: + +.. code:: rst + + .. ifconfig:: releaselevel == 'dev' + .. tags:: internal: needs-review + +-------------------------------+-----------------------------------------------------------------------+ |``tag`` | use case | +===============================+=======================================================================+ diff --git a/doc/devel/tag_guidelines.rst b/doc/devel/tag_guidelines.rst index ca6b8cfde01d..2c80065982bc 100644 --- a/doc/devel/tag_guidelines.rst +++ b/doc/devel/tag_guidelines.rst @@ -1,17 +1,30 @@ -Guidelines for assigning tags to gallery examples -================================================= +Tagging guidelines +================== Why do we need tags? -------------------- Tags serve multiple purposes. -Tags have a one-to-many organization (i.e. one example can have several tags), while the gallery structure requires that examples are placed in only one location. This means tags provide a secondary layer of organization and make the gallery of examples more flexible and more user-friendly. +Tags have a one-to-many organization (i.e. one example can have several tags), while +the gallery structure requires that examples are placed in only one location. This means +tags provide a secondary layer of organization and make the gallery of examples more +flexible and more user-friendly. -They allow for better discoverability, search, and browse functionality. They are helpful for users struggling to write a search query for what they're looking for. +They allow for better discoverability, search, and browse functionality. They are +helpful for users struggling to write a search query for what they're looking for. Hidden tags provide additional functionality for maintainers and contributors. +How to tag? +----------- +Place the tag directive at the bottom of each page and add the tags underneath, e.g.: + +.. code-block:: rst + + .. tags:: + topic: tagging, purpose: reference + What gets a tag? ---------------- @@ -20,56 +33,38 @@ Every gallery example should be tagged with: * 1+ content tags * structural, domain, or internal tag(s) if helpful -Tags can repeat existing forms of organization (e.g. an example is in the Animation folder and also gets an ``animation`` tag). +Tags can repeat existing forms of organization (e.g. an example is in the Animation +folder and also gets an ``animation`` tag). + +Tags are helpful to denote particularly good "byproduct" examples. E.g. the explicit +purpose of a gallery example might be to demonstrate a colormap, but it's also a good +demonstration of a legend. Tag ``legend`` to indicate that, rather than changing the +title or the scope of the example. -Tags are helpful to denote particularly good "byproduct" examples. E.g. the explicit purpose of a gallery example might be to demonstrate a colormap, but it's also a good demonstration of a legend. Tag ``legend`` to indicate that, rather than changing the title or the scope of the example. +.. card:: -**Tag Categories** - See :doc:`Tag Glossary ` for a complete list of tags. + **Tag Categories** + ^^^ + .. rst-class:: section-toc -I. API tags: what content from the API reference is in the example? -II. Structural tags: what format is the example? What context can we provide? -III. Domain tags: what discipline(s) might seek this example consistently? -IV. Internal tags: what information is helpful for maintainers or contributors? + .. toctree:: + :maxdepth: 2 + + tag_glossary + + +++ + See :doc:`Tag Glossary ` for a complete list Proposing new tags ------------------ 1. Review existing tag list, looking out for similar entries (i.e. ``axes`` and ``axis``). -2. If a relevant tag or subcategory does not yet exist, propose it. Each tag is two parts: ``subcategory: tag``. Tags should be one or two words. -3. New tags should be be added when they are relevant to existing gallery entries too. Avoid tags that will link to only a single gallery entry. +2. If a relevant tag or subcategory does not yet exist, propose it. Each tag is two + parts: ``subcategory: tag``. Tags should be one or two words. +3. New tags should be be added when they are relevant to existing gallery entries too. + Avoid tags that will link to only a single gallery entry. 4. Tags can recreate other forms of organization. -Note: Tagging organization aims to work for 80-90% of cases. Some examples fall outside of the tagging structure. Niche or specific examples shouldn't be given standalone tags that won't apply to other examples. - -How to tag? ------------ -Put each tag as a directive at the bottom of the page. - -Related content ---------------- - -What is a gallery example? -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The gallery of examples contains visual demonstrations of matplotlib features. Gallery examples exist so that users can scan through visual examples. - -Unlike tutorials or user guides, gallery examples teach by demonstration, rather than by explanation or instruction. - -Gallery examples should avoid instruction or excessive explanation except for brief clarifying code comments. Instead, they can tag related concepts and/or link to relevant tutorials or user guides. - -Format -^^^^^^ - -All :ref:`examples-index` should aim to follow the following format: - -* Title: 1-6 words, descriptive of content -* Subtitle: 10-50 words, action-oriented description of the example subject -* Image: a clear demonstration of the subject, showing edge cases and different applications if possible -* Code + Text (optional): code, commented as appropriate + written text to add context if necessary - -Example: - -The ``bbox_intersect`` gallery example demonstrates the point of visual examples: - -* this example is "messy" in that it's hard to categorize, but the gallery is the right spot for it because it makes sense to find it by visual search -* https://matplotlib.org/devdocs/gallery/misc/bbox_intersect.html#sphx-glr-gallery-misc-bbox-intersect-py +Tagging organization aims to work for 80-90% of cases. Some examples fall outside of the +tagging structure. Niche or specific examples shouldn't be given standalone tags that +won't apply to other examples. diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index a9f52f0e62b6..72f787eca746 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -182,17 +182,28 @@ circle: plotting a circle using a `matplotlib.patches.Circle` patch vs plotting the circle using the parametric equation of a circle :: from matplotlib.testing.decorators import check_figures_equal - import matplotib.patches as mpatches + import matplotlib.patches as mpatches import matplotlib.pyplot as plt import numpy as np - @check_figures_equal(extensions=['png'], tol=100) + @check_figures_equal() def test_parametric_circle_plot(fig_test, fig_ref): - red_circle_ref = mpatches.Circle((0, 0), 0.2, color='r', clip_on=False) - fig_ref.add_artist(red_circle_ref) - theta = np.linspace(0, 2 * np.pi, 150) + + xo, yo= (.5, .5) radius = 0.4 - fig_test.plot(radius * np.cos(theta), radius * np.sin(theta), color='r') + + ax_test = fig_test.subplots() + theta = np.linspace(0, 2 * np.pi, 150) + l, = ax_test.plot(xo + (radius * np.cos(theta)), + yo + (radius * np.sin(theta)), c='r') + + ax_ref = fig_ref.subplots() + red_circle_ref = mpatches.Circle((xo, yo), radius, ec='r', fc='none', + lw=l.get_linewidth()) + ax_ref.add_artist(red_circle_ref) + + for ax in [ax_ref, ax_test]: + ax.set(xlim=(0,1), ylim=(0,1), aspect='equal') Both comparison decorators have a tolerance argument ``tol`` that is used to specify the tolerance for difference in color value between the two images, where 255 is the maximal @@ -241,7 +252,7 @@ Using tox `Tox `_ is a tool for running tests against multiple Python environments, including multiple versions of Python -(e.g., 3.7, 3.8) and even different Python implementations altogether +(e.g., 3.10, 3.11) and even different Python implementations altogether (e.g., CPython, PyPy, Jython, etc.), as long as all these versions are available on your system's $PATH (consider using your system package manager, e.g. apt-get, yum, or Homebrew, to install them). @@ -258,7 +269,7 @@ You can also run tox on a subset of environments: .. code-block:: bash - $ tox -e py38,py39 + $ 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, diff --git a/doc/index.rst b/doc/index.rst index 1a385d2330af..dedd614985df 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,6 +31,7 @@ Install .. tab-item:: other + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -106,6 +107,7 @@ Community .. grid-item:: + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -144,11 +146,11 @@ Contribute .. grid-item:: - Matplotlib is a community project maintained for and by its users. - - There are many ways you can help! + Matplotlib is a community project maintained for and by its users. See + :ref:`developers-guide-index` for the many ways you can help! .. grid-item:: + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -168,7 +170,7 @@ About us and hard things possible. .. grid-item:: - + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 93c1990b9472..b8f54f346186 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -20,7 +20,7 @@ When installing through a package manager like ``pip`` or ``conda``, the mandatory dependencies are automatically installed. This list is mainly for reference. -* `Python `_ (>= 3.9) +* `Python `_ (>= 3.10) * `contourpy `_ (>= 1.0.1) * `cycler `_ (>= 0.10.0) * `dateutil `_ (>= 2.7) @@ -30,8 +30,6 @@ reference. * `packaging `_ (>= 20.0) * `Pillow `_ (>= 8.0) * `pyparsing `_ (>= 2.3.1) -* `importlib-resources `_ - (>= 3.2.0; only required on Python < 3.10) .. _optional_dependencies: @@ -222,20 +220,19 @@ Build dependencies Python ------ -By default, ``pip`` will build packages using build isolation, meaning that these -build dependencies are temporally installed by pip for the duration of the -Matplotlib build process. However, build isolation is disabled when :ref:`installing Matplotlib for development `; -therefore we recommend using one of our :ref:`virtual environment configurations ` to -create a development environment in which these packages are automatically installed. - -If you are developing Matplotlib and unable to use our environment configurations, -then you must manually install the following packages into your development environment: +``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 +when :ref:`installing Matplotlib for development `, which +means that the dependencies must be explicitly installed, either by :ref:`creating a virtual environment ` +(recommended) or by manually installing the following packages: - `meson-python `_ (>= 0.13.1). - `ninja `_ (>= 1.8.2). This may be available in your package manager or bundled with Meson, but may be installed via ``pip`` if otherwise not available. -- `PyBind11 `_ (>= 2.6). Used to connect C/C++ code +- `PyBind11 `_ (>= 2.13.2). Used to connect C/C++ code with Python. - `setuptools_scm `_ (>= 7). Used to update the reported ``mpl.__version__`` based on the current git commit. @@ -253,37 +250,38 @@ development environment that must be installed before a compiler can be installe You may also need to install headers for various libraries used in the compiled extension source files. +.. _dev-compiler: .. tab-set:: - .. tab-item:: Linux + .. tab-item:: Linux - On some Linux systems, you can install a meta-build package. For example, - on Ubuntu ``apt install build-essential`` + On some Linux systems, you can install a meta-build package. For example, + on Ubuntu ``apt install build-essential`` - Otherwise, use the system distribution's package manager to install - :ref:`gcc `. + Otherwise, use the system distribution's package manager to install + :ref:`gcc `. - .. tab-item:: macOS + .. tab-item:: macOS - Install `Xcode `_ for Apple platform development. + Install `Xcode `_ for Apple platform development. - .. tab-item:: Windows + .. tab-item:: Windows - Install `Visual Studio Build Tools `_ + Install `Visual Studio Build Tools `_ - Make sure "Desktop development with C++" is selected, and that the latest MSVC, - "C++ CMake tools for Windows," and a Windows SDK compatible with your version - of Windows are selected and installed. They should be selected by default under - the "Optional" subheading, but are required to build Matplotlib from source. + Make sure "Desktop development with C++" is selected, and that the latest MSVC, + "C++ CMake tools for Windows," and a Windows SDK compatible with your version + of Windows are selected and installed. They should be selected by default under + the "Optional" subheading, but are required to build Matplotlib from source. - Alternatively, you can install a Linux-like environment such as `CygWin `_ - or `Windows Subsystem for Linux `_. - If using `MinGW-64 `_, we require **v6** of the - ```Mingw-w64-x86_64-headers``. + Alternatively, you can install a Linux-like environment such as `CygWin `_ + or `Windows Subsystem for Linux `_. + If using `MinGW-64 `_, we require **v6** of the + ```Mingw-w64-x86_64-headers``. -We highly recommend that you install a compiler using your platform tool, i.e., -Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: +We highly recommend that you install a compiler using your platform tool, i.e., Xcode, +VS Code or Linux package manager. Choose **one** compiler from this list: .. _compiler-table: @@ -310,7 +308,6 @@ Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: - `Visual Studio 2019 C++ `_ - .. _test-dependencies: Test dependencies @@ -330,8 +327,11 @@ Optional In addition to all of the optional dependencies on the main library, for testing the following will be used if they are installed. -- Ghostscript_ (>= 9.0, to render PDF files) -- Inkscape_ (to render SVG files) +Python +^^^^^^ +These packages are installed when :ref:`creating a virtual environment `, +otherwise they must be installed manually: + - nbformat_ and nbconvert_ used to test the notebook backend - pandas_ used to test compatibility with Pandas - pikepdf_ used in some tests for the pgf and pdf backends @@ -343,9 +343,14 @@ testing the following will be used if they are installed. - pytest-xvfb_ to run tests without windows popping up (Linux) - pytz_ used to test pytz int - sphinx_ used to test our sphinx extensions +- xarray_ used to test compatibility with xarray + +External tools +^^^^^^^^^^^^^^ +- Ghostscript_ (>= 9.0, to render PDF files) +- Inkscape_ (to render SVG files) - `WenQuanYi Zen Hei`_ and `Noto Sans CJK`_ fonts for testing font fallback and non-Western fonts -- xarray_ used to test compatibility with xarray If any of these dependencies are not discovered, then the tests that rely on them will be skipped by pytest. @@ -358,6 +363,7 @@ them will be skipped by pytest. .. _Ghostscript: https://ghostscript.com/ .. _Inkscape: https://inkscape.org +.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _flake8: https://pypi.org/project/flake8/ .. _nbconvert: https://pypi.org/project/nbconvert/ .. _nbformat: https://pypi.org/project/nbformat/ @@ -372,7 +378,6 @@ them will be skipped by pytest. .. _pytest-xvfb: https://pypi.org/project/pytest-xvfb/ .. _pytest: http://doc.pytest.org/en/latest/ .. _sphinx: https://pypi.org/project/Sphinx/ -.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _Noto Sans CJK: https://fonts.google.com/noto/use .. _xarray: https://pypi.org/project/xarray/ @@ -397,14 +402,15 @@ The content of :file:`doc-requirements.txt` is also shown below: :literal: +.. _doc-dependencies-external: + External tools -------------- -The documentation requires LaTeX and Graphviz. These are not -Python packages and must be installed separately. - Required ^^^^^^^^ +The documentation requires LaTeX and Graphviz. These are not +Python packages and must be installed separately. * `Graphviz `_ * a minimal working LaTeX distribution, e.g. `TeX Live `_ or @@ -412,15 +418,14 @@ Required The following LaTeX packages: - * `dvipng `_ - * `underscore `_ - * `cm-super `_ - * ``collection-fontsrecommended`` +* `dvipng `_ +* `underscore `_ +* `cm-super `_ +* ``collection-fontsrecommended`` The complete version of many LaTex distribution installers, e.g. "texlive-full" or "texlive-all", will often automatically include these packages. - Optional ^^^^^^^^ @@ -429,5 +434,5 @@ process will raise various warnings. * `Inkscape `_ * `optipng `_ -* the font `xkcd script `_ or `Comic Neue `_ +* 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 867e4600a77e..99ccc163a82e 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -267,9 +267,9 @@ at the Terminal.app command line:: You should see something like :: - 3.6.0 /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/__init__.py + 3.10.0 /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/matplotlib/__init__.py -where ``3.6.0`` is the Matplotlib version you just installed, and the path +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 :: diff --git a/doc/make.bat b/doc/make.bat index 37c74eb5079a..09d76404e60f 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -36,6 +36,7 @@ if "%1" == "show" goto show if "%1" == "clean" ( REM workaround because sphinx does not completely clean up (#11139) rmdir /s /q "%SOURCEDIR%\build" + rmdir /s /q "%SOURCEDIR%\_tags" rmdir /s /q "%SOURCEDIR%\api\_as_gen" rmdir /s /q "%SOURCEDIR%\gallery" rmdir /s /q "%SOURCEDIR%\plot_types" diff --git a/doc/missing-references.json b/doc/missing-references.json index 1b0a6f9ef226..1a816d19f7cd 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -1,8 +1,7 @@ { "py:attr": { "cbar_axes": [ - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:72", - "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:72" + "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:72" ], "eventson": [ "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.CheckButtons.set_active:4", @@ -36,40 +35,33 @@ "lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar.Colorbar.add_lines:4" ], "matplotlib.axes.Axes.patch": [ - "doc/tutorials/artists.rst:188", - "doc/tutorials/artists.rst:427" + "doc/tutorials/artists.rst:185", + "doc/tutorials/artists.rst:424" ], "matplotlib.axes.Axes.patches": [ - "doc/tutorials/artists.rst:465" + "doc/tutorials/artists.rst:462" ], "matplotlib.axes.Axes.transAxes": [ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:8" ], "matplotlib.axes.Axes.transData": [ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox:11", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse:33", "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:8" ], "matplotlib.axes.Axes.xaxis": [ - "doc/tutorials/artists.rst:611", + "doc/tutorials/artists.rst:608", "doc/users/explain/axes/axes_intro.rst:133" ], "matplotlib.axes.Axes.yaxis": [ - "doc/tutorials/artists.rst:611", + "doc/tutorials/artists.rst:608", "doc/users/explain/axes/axes_intro.rst:133" ], - "matplotlib.axis.Axis.label": [ - "doc/tutorials/artists.rst:658" - ], - "matplotlib.colors.Colormap.name": [ - "lib/matplotlib/cm.py:docstring of matplotlib.cm.register_cmap:14" - ], "matplotlib.figure.Figure.patch": [ - "doc/tutorials/artists.rst:188", - "doc/tutorials/artists.rst:321" + "doc/tutorials/artists.rst:185", + "doc/tutorials/artists.rst:318" ], "matplotlib.figure.Figure.transFigure": [ - "doc/tutorials/artists.rst:370" + "doc/tutorials/artists.rst:367" ], "max": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.p1:4" @@ -84,29 +76,26 @@ "lib/matplotlib/scale.py:docstring of matplotlib.scale.ScaleBase:8" ], "output_dims": [ - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.AitoffTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.InvertedAitoffTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.HammerTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.InvertedHammerTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.InvertedLambertTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.LambertTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.InvertedMollweideTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.MollweideTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:20" + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.AitoffTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.InvertedAitoffTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.HammerTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.InvertedHammerTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.InvertedLambertTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.LambertTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.InvertedMollweideTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.MollweideTransform.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:16", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:16", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:22" ], "triangulation": [ "lib/matplotlib/tri/_trirefine.py:docstring of matplotlib.tri._trirefine.UniformTriRefiner.refine_triangulation:2" ], - "use_sticky_edges": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:53" - ], "width": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.bounds:2" ], @@ -125,19 +114,19 @@ }, "py:class": { "HashableList[_HT]": [ - "doc/docstring of builtins.list:17" + ":1" ], "matplotlib.axes._base._AxesBase": [ "doc/api/artist_api.rst:202" ], "matplotlib.backend_bases.FigureCanvas": [ - "doc/tutorials/artists.rst:36", - "doc/tutorials/artists.rst:38", - "doc/tutorials/artists.rst:43" + "doc/tutorials/artists.rst:33", + "doc/tutorials/artists.rst:35", + "doc/tutorials/artists.rst:40" ], "matplotlib.backend_bases.Renderer": [ - "doc/tutorials/artists.rst:38", - "doc/tutorials/artists.rst:43" + "doc/tutorials/artists.rst:35", + "doc/tutorials/artists.rst:40" ], "matplotlib.backend_bases._Backend": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ShowBase:1" @@ -238,45 +227,53 @@ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Scaled:1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesHostAxes": [ - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:32::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesParasite": [ - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:32::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1" ], "mpl_toolkits.axisartist.Axes": [ "doc/api/toolkits/axisartist.rst:6" ], "mpl_toolkits.axisartist.axisline_style.AxislineStyle._Base": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle.SimpleArrow:1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.FilledArrow": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + ":1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + ":1" ], "mpl_toolkits.axisartist.axislines._FixedAxisArtistHelperBase": [ + ":1", "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.FixedAxisArtistHelperRectilinear:1", "lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py:docstring of mpl_toolkits.axisartist.grid_helper_curvelinear.FixedAxisArtistHelper:1" ], "mpl_toolkits.axisartist.axislines._FloatingAxisArtistHelperBase": [ + ":1", "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.FloatingAxisArtistHelperRectilinear:1", "lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py:docstring of mpl_toolkits.axisartist.grid_helper_curvelinear.FloatingAxisArtistHelper:1" ], "mpl_toolkits.axisartist.floating_axes.FloatingAxesHostAxes": [ - "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:34::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" + ], + "numpy.float64": [ + "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" ], "numpy.uint8": [ - "lib/matplotlib/path.py:docstring of matplotlib.path:1" + ":1" ] }, "py:data": { "matplotlib.axes.Axes.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:248", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:249", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:201", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:249", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:248" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:250", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:251", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:209", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:251", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:250" ] }, "py:meth": { @@ -297,92 +294,70 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:9" ], "matplotlib.collections._CollectionWithSizes.set_sizes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:176", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:82", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:118", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:118", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:206", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:178", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:212", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.AsteriskPolygonCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.BrokenBarHCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.CircleCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.PathCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:44", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:176", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:82", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:118", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:118", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:206", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:178", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:212", - "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Barbs.set:45", - "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Quiver.set:45", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:209", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:248", - "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", - "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:180", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:85", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:122", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:122", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:218", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:187", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:256", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.scatter:174", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.AsteriskPolygonCollection.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.CircleCollection.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.FillBetweenPolyCollection.set:46", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.PathCollection.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyCollection.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:45", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:45", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:180", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:85", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:122", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:122", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:218", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:187", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:256", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.scatter:174", + "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Barbs.set:46", + "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Quiver.set:46", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:213", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:292", + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:47", + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:46" ], "matplotlib.collections._MeshData.set_array": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:160", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:168", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:17", "lib/matplotlib/collections.py:docstring of matplotlib.artist.QuadMesh.set:17", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:160" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:168" ] }, "py:obj": { "Artist.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:323" ], - "Artist.sticky_edges": [ - "doc/api/axes_api.rst:356::1" - ], - "Axes.dataLim": [ - "doc/api/axes_api.rst:293::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:2" - ], - "AxesBase": [ - "doc/api/axes_api.rst:448::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:2" - ], "Figure.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:333" ], - "Glyph": [ - "doc/gallery/misc/ftface_props.rst:28" - ], "Image": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" ], - "ImageComparisonFailure": [ - "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators.image_comparison:2" - ], "Line2D.pick": [ - "doc/users/explain/figure/event_handling.rst:568" - ], - "QuadContourSet.changed()": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:152", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:152", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:152", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:152" + "doc/users/explain/figure/event_handling.rst:571" ], "Rectangle.contains": [ - "doc/users/explain/figure/event_handling.rst:280" + "doc/users/explain/figure/event_handling.rst:285" ], "Size.from_any": [ - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:84", - "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:84" + "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:87" ], "Timer": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.FigureCanvasBase.new_timer:2", "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.TimerBase:14" ], "ToolContainer": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:2", - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:20" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:8", + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:9" ], "_iter_collection": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:15", @@ -399,22 +374,14 @@ "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.PathEffectRenderer.draw_path_collection:15" ], "_read": [ - "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:20" + "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:22" ], "active": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:34" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:21" ], "ax.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.indicate_inset:19", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.inset_axes:11" ], - "axes.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:144", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:145", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:97", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:145", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:144" - ], "can_composite": [ "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" ], @@ -424,13 +391,6 @@ "draw_image": [ "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.option_scale_image:2" ], - "figure.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:144", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:145", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:97", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:145", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:144" - ], "fmt_xdata": [ "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_xdata:4" ], @@ -441,43 +401,40 @@ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size:1" ], "ipykernel.pylab.backend_inline": [ - "doc/users/explain/figure/interactive.rst:340" + "doc/users/explain/figure/interactive.rst:361" ], "kde.covariance_factor": [ - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:41" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:29" ], "kde.factor": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:46", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:58", "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:12", - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:45", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:46" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:33", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:58" ], "make_image": [ "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" ], "matplotlib.animation.ArtistAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" - ], - "matplotlib.animation.ArtistAnimation.repeat": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:33::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.FFMpegFileWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" @@ -549,22 +506,19 @@ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.finish:1::1" ], "matplotlib.animation.FuncAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" - ], - "matplotlib.animation.FuncAnimation.repeat": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.HTMLWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:27::1" @@ -639,55 +593,43 @@ "doc/api/_as_gen/matplotlib.animation.PillowWriter.rst:26::1" ], "matplotlib.animation.TimedAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.typing._HT": [ - "doc/docstring of builtins.list:17" + ":1" ], "mpl_toolkits.axislines.Axes": [ "lib/mpl_toolkits/axisartist/axis_artist.py:docstring of mpl_toolkits.axisartist.axis_artist:7" ], - "next_whats_new": [ - "doc/users/next_whats_new/README.rst:6" - ], "option_scale_image": [ "lib/matplotlib/backends/backend_cairo.py:docstring of matplotlib.backends.backend_cairo.RendererCairo.draw_image:22", "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_image:22", "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_image:22", "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template.RendererTemplate.draw_image:22" ], - "print_xyz": [ - "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template:22" - ], "toggled": [ "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.disable:4", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.enable:4", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.trigger:2", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ZoomPanBase.trigger:2" - ], - "tool_removed_event": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:6" - ], - "whats_new.rst": [ - "doc/users/next_whats_new/README.rst:6" ] } } diff --git a/doc/project/citing.rst b/doc/project/citing.rst index 544c899da4d2..be58473a26e4 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.9.4 + .. image:: ../_static/zenodo_cache/14436121.svg + :target: https://doi.org/10.5281/zenodo.14436121 v3.9.3 .. image:: ../_static/zenodo_cache/14249941.svg :target: https://doi.org/10.5281/zenodo.14249941 diff --git a/doc/sphinxext/missing_references.py b/doc/sphinxext/missing_references.py index c621adb2c945..87432bc524b4 100644 --- a/doc/sphinxext/missing_references.py +++ b/doc/sphinxext/missing_references.py @@ -17,11 +17,9 @@ from collections import defaultdict import json -import logging from pathlib import Path from docutils.utils import get_source_line -from docutils import nodes from sphinx.util import logging as sphinx_logging import matplotlib @@ -29,59 +27,6 @@ logger = sphinx_logging.getLogger(__name__) -class MissingReferenceFilter(logging.Filter): - """ - A logging filter designed to record missing reference warning messages - for use by this extension - """ - def __init__(self, app): - self.app = app - super().__init__() - - def _record_reference(self, record): - if not (getattr(record, 'type', '') == 'ref' and - isinstance(getattr(record, 'location', None), nodes.Node)): - return - - if not hasattr(self.app.env, "missing_references_warnings"): - self.app.env.missing_references_warnings = defaultdict(set) - - record_missing_reference(self.app, - self.app.env.missing_references_warnings, - record.location) - - def filter(self, record): - self._record_reference(record) - return True - - -def record_missing_reference(app, record, node): - domain = node["refdomain"] - typ = node["reftype"] - target = node["reftarget"] - location = get_location(node, app) - - domain_type = f"{domain}:{typ}" - - record[(domain_type, target)].add(location) - - -def record_missing_reference_handler(app, env, node, contnode): - """ - When the sphinx app notices a missing reference, it emits an - event which calls this function. This function records the missing - references for analysis at the end of the sphinx build. - """ - if not app.config.missing_references_enabled: - # no-op when we are disabled. - return - - if not hasattr(env, "missing_references_events"): - env.missing_references_events = defaultdict(set) - - record_missing_reference(app, env.missing_references_events, node) - - def get_location(node, app): """ Given a docutils node and a sphinx application, return a string @@ -146,10 +91,35 @@ def _truncate_location(location): return location.split(":", 1)[0] -def _warn_unused_missing_references(app): - if not app.config.missing_references_warn_unused_ignores: - return +def handle_missing_reference(app, domain, node): + """ + Handle the warn-missing-reference Sphinx event. + + This function will: + #. record missing references for saving/comparing with ignored list. + #. prevent Sphinx from raising a warning on ignored references. + """ + refdomain = node["refdomain"] + reftype = node["reftype"] + target = node["reftarget"] + location = get_location(node, app) + domain_type = f"{refdomain}:{reftype}" + + app.env.missing_references_events[(domain_type, target)].add(location) + + # If we're ignoring this event, return True so that Sphinx thinks we handled it, + # even though we didn't print or warn. If we aren't ignoring it, Sphinx will print a + # warning about the missing reference. + if location in app.env.missing_references_ignored_references.get( + (domain_type, target), []): + return True + + +def warn_unused_missing_references(app, exc): + """ + Check that all lines of the existing JSON file are still necessary. + """ # We can only warn if we are building from a source install # otherwise, we just have to skip this step. basepath = Path(matplotlib.__file__).parent.parent.parent.resolve() @@ -159,9 +129,8 @@ def _warn_unused_missing_references(app): return # This is a dictionary of {(domain_type, target): locations} - references_ignored = getattr( - app.env, 'missing_references_ignored_references', {}) - references_events = getattr(app.env, 'missing_references_events', {}) + references_ignored = app.env.missing_references_ignored_references + references_events = app.env.missing_references_events # Warn about any reference which is no longer missing. for (domain_type, target), locations in references_ignored.items(): @@ -184,26 +153,13 @@ def _warn_unused_missing_references(app): subtype=domain_type) -def save_missing_references_handler(app, exc): +def save_missing_references(app, exc): """ - At the end of the sphinx build, check that all lines of the existing JSON - file are still necessary. - - If the configuration value ``missing_references_write_json`` is set - then write a new JSON file containing missing references. + Write a new JSON file containing missing references. """ - if not app.config.missing_references_enabled: - # no-op when we are disabled. - return - - _warn_unused_missing_references(app) - json_path = Path(app.confdir) / app.config.missing_references_filename - - references_warnings = getattr(app.env, 'missing_references_warnings', {}) - - if app.config.missing_references_write_json: - _write_missing_references_json(references_warnings, json_path) + references_warnings = app.env.missing_references_events + _write_missing_references_json(references_warnings, json_path) def _write_missing_references_json(records, json_path): @@ -220,6 +176,7 @@ def _write_missing_references_json(records, json_path): transformed_records[domain_type][target] = sorted(paths) with json_path.open("w") as stream: json.dump(transformed_records, stream, sort_keys=True, indent=2) + stream.write("\n") # Silence pre-commit no-newline-at-end-of-file warning. def _read_missing_references_json(json_path): @@ -242,49 +199,25 @@ def _read_missing_references_json(json_path): return ignored_references -def prepare_missing_references_handler(app): +def prepare_missing_references_setup(app): """ - Handler called to initialize this extension once the configuration - is ready. - - Reads the missing references file and populates ``nitpick_ignore`` if - appropriate. + Initialize this extension once the configuration is ready. """ if not app.config.missing_references_enabled: # no-op when we are disabled. return - sphinx_logger = logging.getLogger('sphinx') - missing_reference_filter = MissingReferenceFilter(app) - for handler in sphinx_logger.handlers: - if (isinstance(handler, sphinx_logging.WarningStreamHandler) - and missing_reference_filter not in handler.filters): - - # This *must* be the first filter, because subsequent filters - # throw away the node information and then we can't identify - # the reference uniquely. - handler.filters.insert(0, missing_reference_filter) - - app.env.missing_references_ignored_references = {} + app.connect("warn-missing-reference", handle_missing_reference) + if app.config.missing_references_warn_unused_ignores: + app.connect("build-finished", warn_unused_missing_references) + if app.config.missing_references_write_json: + app.connect("build-finished", save_missing_references) json_path = Path(app.confdir) / app.config.missing_references_filename - if not json_path.exists(): - return - - ignored_references = _read_missing_references_json(json_path) - - app.env.missing_references_ignored_references = ignored_references - - # If we are going to re-write the JSON file, then don't suppress missing - # reference warnings. We want to record a full list of missing references - # for use later. Otherwise, add all known missing references to - # ``nitpick_ignore``` - if not app.config.missing_references_write_json: - # Since Sphinx v6.2, nitpick_ignore may be a list, set or tuple, and - # defaults to set. Previously it was always a list. Cast to list for - # consistency across versions. - app.config.nitpick_ignore = list(app.config.nitpick_ignore) - app.config.nitpick_ignore.extend(ignored_references.keys()) + app.env.missing_references_ignored_references = ( + _read_missing_references_json(json_path) if json_path.exists() else {} + ) + app.env.missing_references_events = defaultdict(set) def setup(app): @@ -294,8 +227,6 @@ def setup(app): app.add_config_value("missing_references_filename", "missing-references.json", "env") - app.connect("builder-inited", prepare_missing_references_handler) - app.connect("missing-reference", record_missing_reference_handler) - app.connect("build-finished", save_missing_references_handler) + app.connect("builder-inited", prepare_missing_references_setup) return {'parallel_read_safe': True} diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index bec073081a68..c12a983aa6a8 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,111 +1,590 @@ .. _github-stats: -GitHub statistics for 3.9.3 (Nov 30, 2024) -========================================== +GitHub statistics for 3.10.0 (Dec 13, 2024) +=========================================== -GitHub statistics for 2024/08/12 (tag: v3.9.2) - 2024/11/30 +GitHub statistics for 2024/05/15 (tag: v3.9.0) - 2024/12/13 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 6 issues and merged 62 pull requests. -The full list can be seen `on GitHub `__ +We closed 100 issues and merged 337 pull requests. +The full list can be seen `on GitHub `__ -The following 18 authors contributed 90 commits. +The following 128 authors contributed 1932 commits. -* Andresporcruz +* abhi-jha +* Adam J. Stewart +* Aditi Gautam +* Aditya Vidyadhar Kamath +* Aishling Cooke +* Alan +* Alan Sosa +* Alice +* Aman Nijjar +* Ammar Qazi +* Ancheng +* anpaulan +* Anson0028 +* Anthony Lee +* anTon * Antony Lee +* Ayoub Gouasmi +* Brigitta Sipőcz +* Caitlin Hathaway +* cesar * Charlie LeWarne +* Christian Mattsson +* ClarkeAC +* Clemens Brunner +* Clement Gilli +* cmp0xff +* Costa Paraskevopoulos +* dale +* Dani Pinyol +* Daniel Weiss +* Danny +* David Bakaj +* David Lowry-Duda +* David Meyer +* David Stansby +* dbakaj * dependabot[bot] +* Diogo Cardoso +* Doron Behar +* Edgar Andrés Margffoy Tuay * Elliott Sales de Andrade +* Eytan Adler +* farquh +* Felipe Cybis Pereira +* Filippo Balzaretti +* FMasson +* Francisco Cardozo * Gavin S * Greg Lucas +* haaris * hannah +* Ian Thomas +* Illviljan +* James Addison +* James Spencer +* Jody Klymak +* john +* Jonas Eschle +* Jouni K. Seppänen +* juanis2112 +* Juanita Gomez +* Justin Hendrick +* K900 +* Kaustbh +* Kaustubh +* Kherim Willems * Kyle Sunden * Kyra Cho -* kyracho +* Larry Bradley +* litchi +* Lorenzo +* Lucx33 * Lumberbot (aka Jack) +* MadPhysicist +* malhar2460 +* Martino Sorbaro +* Mathias Hauser +* Matthew Feickert +* Matthew Petroff +* Melissa Weber Mendonça +* Michael +* Michael Droettboom * Michael Hinton +* MischaMegens2 +* Moritz Wolter +* muchojp +* Nabil +* nakamura yuki +* odile +* OdileVidrine * Oscar Gustafsson +* Panicks28 +* Paul An +* Pedro Barão +* PedroBittarBarao +* Peter Talley +* Pierre-antoine Comby +* Pranav +* Pranav Raghu +* pre-commit-ci[bot] +* proximalf +* r3kste +* Randolf Scholz +* Refael Ackermann +* RickyP24 +* rnhmjoj * Ruth Comer +* Ryan May +* Sai Chaitanya, Sanivada +* saranti +* scaccol +* Scott Shambaugh +* Sean Smith +* Simon May +* simond07 +* smcgrawDotNet +* Takumasa N +* Takumasa N. +* Takumasa Nakamura +* thiagoluisbecker * Thomas A Caswell +* Tiago Lubiana * Tim Hoffmann +* trananso +* Trygve Magnus Ræder +* Victor Liu * vittoboa +* Xeniya Shoiko GitHub issues and pull requests: -Pull Requests (62): +Pull Requests (337): -* :ghpull:`29195`: Backport PR #29191 on branch v3.9.x (ci: Simplify 3.13t test setup) -* :ghpull:`29191`: ci: Simplify 3.13t test setup -* :ghpull:`29176`: Backport PR #29148 on branch v3.9.x (Don't fail on equal-but-differently-named cmaps in qt figureoptions.) -* :ghpull:`29148`: Don't fail on equal-but-differently-named cmaps in qt figureoptions. -* :ghpull:`29165`: Backport PR #29153 on branch v3.9.x (Bump codecov/codecov-action from 4 to 5 in the actions group) -* :ghpull:`29153`: Bump codecov/codecov-action from 4 to 5 in the actions group -* :ghpull:`29149`: Backport CI config updates to v3.9.x -* :ghpull:`29121`: Backport PR #29120 on branch v3.9.x (DOC: Switch nested pie example from cmaps to color_sequences) -* :ghpull:`29071`: Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 in the actions group -* :ghpull:`29046`: Backport PR #28981 on branch v3.9.x (FIX: macos: Use standard NSApp run loop in our input hook) -* :ghpull:`28981`: FIX: macos: Use standard NSApp run loop in our input hook -* :ghpull:`29041`: Backport PR #29035 on branch v3.9.x (FIX: Don't set_wmclass on GTK3) -* :ghpull:`29035`: FIX: Don't set_wmclass on GTK3 -* :ghpull:`29037`: Backport PR #29036 on branch v3.9.x (Don't pass redundant inline=True to example clabel() calls.) -* :ghpull:`29032`: Backport PR #27569 on branch v3.9.x (DOC: initial tags for statistics section of gallery) -* :ghpull:`29034`: Backport PR #29031 on branch v3.9.x (DOC: Fix copy-paste typo in ColorSequenceRegistry) -* :ghpull:`29031`: DOC: Fix copy-paste typo in ColorSequenceRegistry -* :ghpull:`29015`: Backport PR #29014 on branch v3.9.x (FIX: fake out setuptools scm in tox on ci) -* :ghpull:`29014`: FIX: fake out setuptools scm in tox on ci -* :ghpull:`29010`: Backport PR #29005 on branch v3.9.x (DOC: Update meson-python intersphinx link) -* :ghpull:`29006`: Backport PR #28993 on branch v3.9.x (FIX: contourf hatches use multiple edgecolors) -* :ghpull:`28993`: FIX: contourf hatches use multiple edgecolors -* :ghpull:`28988`: Backport PR #28987 on branch v3.9.x (Fix: Do not use numeric tolerances for axline special cases) -* :ghpull:`28947`: Backport PR #28925 on branch v3.9.x (TST: handle change in pytest.importorskip behavior) -* :ghpull:`28989`: Backport PR #28972 on branch v3.9.x (Switch macOS 12 runner images to macOS 13) -* :ghpull:`28972`: Switch macOS 12 runner images to macOS 13 -* :ghpull:`28987`: Fix: Do not use numeric tolerances for axline special cases -* :ghpull:`28954`: Backport PR #28952 on branch v3.9.x (BLD: update trove metadata to support py3.13) -* :ghpull:`28952`: BLD: update trove metadata to support py3.13 -* :ghpull:`28887`: Backport PR #28883 on branch v3.9.x (Only check X11 when running Tkinter tests) -* :ghpull:`28926`: Backport PR #28689 on branch v3.9.x (ci: Enable testing on Python 3.13) -* :ghpull:`28925`: TST: handle change in pytest.importorskip behavior -* :ghpull:`28945`: Backport PR #28943 on branch v3.9.x (DOC: Clarify the returned line of axhline()/axvline()) -* :ghpull:`28939`: Backport PR #28900 on branch v3.9.x (DOC: Improve fancybox demo) -* :ghpull:`28900`: DOC: Improve fancybox demo -* :ghpull:`28902`: Backport PR #28881 on branch v3.9.x (Fix ``axline`` for slopes <= 1E-8. Closes #28386) -* :ghpull:`28431`: Fix ``axline`` for slopes < 1E-8 -* :ghpull:`28881`: Fix ``axline`` for slopes <= 1E-8. Closes #28386 -* :ghpull:`28883`: Only check X11 when running Tkinter tests -* :ghpull:`28859`: Backport PR #28858 on branch v3.9.x (Fix flaky labelcolor tests) -* :ghpull:`28858`: Fix flaky labelcolor tests -* :ghpull:`28839`: Backport PR #28836 on branch v3.9.x (MNT: Use __init__ parameters of font properties) -* :ghpull:`28836`: MNT: Use __init__ parameters of font properties -* :ghpull:`28828`: Backport PR #28818 on branch v3.9.x (Resolve configdir so that it's not a symlink when is_dir() is called) -* :ghpull:`28818`: Resolve configdir so that it's not a symlink when is_dir() is called -* :ghpull:`28811`: Backport PR #28810 on branch v3.9.x (Document how to obtain sans-serif usetex math.) -* :ghpull:`28806`: Backport PR #28805 on branch v3.9.x (add brackets to satisfy the new sequence requirement) -* :ghpull:`28802`: Backport PR #28798 on branch v3.9.x (DOC: Correctly list modules that have been internalized) -* :ghpull:`28791`: Backport PR #28790 on branch v3.9.x (DOC: Fix duplicate Figure.set_dpi entry) -* :ghpull:`28787`: Backport PR #28706 on branch v3.9.x (Add Returns info to to_jshtml docstring) -* :ghpull:`28706`: Add Returns info to to_jshtml docstring -* :ghpull:`28751`: Backport PR #28271 on branch v3.9.x (Fix draggable legend disappearing when picking while use_blit=True) -* :ghpull:`28271`: Fix draggable legend disappearing when picking while use_blit=True -* :ghpull:`28747`: Backport PR #28743 on branch v3.9.x (Minor fixes in ticker docs) -* :ghpull:`28743`: Minor fixes in ticker docs -* :ghpull:`28738`: Backport PR #28737 on branch v3.9.x (TST: Fix image comparison directory for test_striped_lines) -* :ghpull:`28740`: Backport PR #28739 on branch v3.9.x (Tweak interactivity docs wording (and fix capitalization).) -* :ghpull:`28737`: TST: Fix image comparison directory for test_striped_lines -* :ghpull:`28733`: Backport PR #28732 on branch v3.9.x (Renames the minumumSizeHint method to minimumSizeHint) -* :ghpull:`28732`: Renames the minumumSizeHint method to minimumSizeHint -* :ghpull:`28689`: ci: Enable testing on Python 3.13 -* :ghpull:`28724`: Backport fixes from #28711 +* :ghpull:`29299`: Merge v3.9.x into v3.10.x +* :ghpull:`29296`: Backport PR #29295 on branch v3.10.x (BLD: Pin meson-python to <0.17.0) +* :ghpull:`29290`: Backport PR #29254 on branch v3.10.x (DOC: Add note to align_labels()) +* :ghpull:`29289`: Backport PR #29260 on branch v3.10.x (DOC: Better explanation of rcParams "patch.edgecolor" and "patch.force_edgecolor") +* :ghpull:`29288`: Backport PR #29285 on branch v3.10.x (Retarget PR#29175 to main) +* :ghpull:`29254`: DOC: Add note to align_labels() +* :ghpull:`29260`: DOC: Better explanation of rcParams "patch.edgecolor" and "patch.force_edgecolor" +* :ghpull:`29285`: Retarget PR#29175 to main +* :ghpull:`29286`: Backport PR #29274 on branch v3.10.x (Bump the actions group across 1 directory with 2 updates) +* :ghpull:`29274`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`29283`: Backport PR #29272 on branch v3.10.x (DOC: Add section on translating between Axes and pyplot interface) +* :ghpull:`29272`: DOC: Add section on translating between Axes and pyplot interface +* :ghpull:`29279`: Backport PR #29265 on branch v3.10.x (DOC: Slightly improve the LineCollection docstring) +* :ghpull:`29276`: Backport PR #29247 on branch v3.10.x (Fix building freetype 2.6.1 on macOS clang 18) +* :ghpull:`29244`: Switch to a 3d rotation trackball implementation with path independence +* :ghpull:`29265`: DOC: Slightly improve the LineCollection docstring +* :ghpull:`29247`: Fix building freetype 2.6.1 on macOS clang 18 +* :ghpull:`29268`: Bump the actions group with 2 updates +* :ghpull:`29266`: Backport PR #29251 on branch v3.10.x (Zizmor audit) +* :ghpull:`29269`: Backport PR #29267 on branch v3.10.x (Exclude pylab from mypy checks) +* :ghpull:`29267`: Exclude pylab from mypy checks +* :ghpull:`29251`: Zizmor audit +* :ghpull:`29255`: Backport PR #29249 on branch v3.10.x ([Bug Fix] Fix reverse mapping for _translate_tick_params) +* :ghpull:`29249`: [Bug Fix] Fix reverse mapping for _translate_tick_params +* :ghpull:`29250`: Backport PR #29243 on branch v3.10.x (Add quotes around [dev] in environment.yml) +* :ghpull:`29243`: Add quotes around [dev] in environment.yml +* :ghpull:`29246`: Backport PR #29240 on branch v3.10.x (DOC: Add plt.show() to introductory pyplot example) +* :ghpull:`29240`: DOC: Add plt.show() to introductory pyplot example +* :ghpull:`29239`: Backport PR #29236 on branch v3.10.x (ANI: Reduce Pillow frames to RGB when opaque) +* :ghpull:`29238`: Backport PR #29167 on branch v3.10.x (BUGFIX: use axes unit information in ConnectionPatch ) +* :ghpull:`29236`: ANI: Reduce Pillow frames to RGB when opaque +* :ghpull:`29167`: BUGFIX: use axes unit information in ConnectionPatch +* :ghpull:`29232`: Merge branch v3.9.x into v3.10.x +* :ghpull:`29230`: Backport PR #29188 on branch v3.10.x (Bump pypa/cibuildwheel from 2.21.3 to 2.22.0 in the actions group) +* :ghpull:`29188`: Bump pypa/cibuildwheel from 2.21.3 to 2.22.0 in the actions group +* :ghpull:`29225`: Backport PR #29213 on branch v3.10.x (avoid-unnecessary-warning-in-_pcolorargs-function) +* :ghpull:`29211`: Backport PR #29133 on branch v3.10.x (Creating_parse_bar_color_args to unify color handling in plt.bar with precedence and sequence support for facecolor and edgecolor) +* :ghpull:`29177`: Backport PR #29148 on branch v3.10.x (Don't fail on equal-but-differently-named cmaps in qt figureoptions.) +* :ghpull:`29226`: Backport PR #29206 on branch v3.10.x (Skip more tests on pure-Wayland systems) +* :ghpull:`29206`: Skip more tests on pure-Wayland systems +* :ghpull:`29213`: avoid-unnecessary-warning-in-_pcolorargs-function +* :ghpull:`29210`: Backport PR #29209 on branch v3.10.x (FIX: pcolormesh with no x y args and nearest interp) +* :ghpull:`29133`: Creating_parse_bar_color_args to unify color handling in plt.bar with precedence and sequence support for facecolor and edgecolor +* :ghpull:`29209`: FIX: pcolormesh with no x y args and nearest interp +* :ghpull:`29200`: Backport PR #29182 on branch v3.10.x (Update backend_qt.py: parent not passed to __init__ on subplottool) +* :ghpull:`29207`: Backport PR #29169 on branch v3.10.x (Minor fixes to text intro explainer) +* :ghpull:`29169`: Minor fixes to text intro explainer +* :ghpull:`29159`: Pending warning for deprecated parameter 'vert' of box and violin on 3.10 +* :ghpull:`29196`: Backport PR #29191 on branch v3.10.x (ci: Simplify 3.13t test setup) +* :ghpull:`29182`: Update backend_qt.py: parent not passed to __init__ on subplottool +* :ghpull:`29189`: Backport PR #28934 on branch v3.10.x (ci: Unpin micromamba again) +* :ghpull:`29186`: Backport PR #28335 on branch v3.10.x (DOC: do not posting LLM output as your own work) +* :ghpull:`28934`: ci: Unpin micromamba again +* :ghpull:`28335`: DOC: do not posting LLM output as your own work +* :ghpull:`29178`: Backport PR #29163 on branch v3.9.x (ci: Remove outdated pkg-config package on macOS) +* :ghpull:`29170`: Backport PR #29154 on branch v3.10.x (Relax conditions for warning on updating converters) +* :ghpull:`29154`: Relax conditions for warning on updating converters +* :ghpull:`29166`: Backport PR #29153 on branch v3.10.x (Bump codecov/codecov-action from 4 to 5 in the actions group) +* :ghpull:`29164`: Backport PR #29163 on branch v3.10.x (ci: Remove outdated pkg-config package on macOS) +* :ghpull:`29168`: Backport PR #29073 on branch v3.10.x (Update secondary_axis tutorial) +* :ghpull:`29073`: Update secondary_axis tutorial +* :ghpull:`29163`: ci: Remove outdated pkg-config package on macOS +* :ghpull:`29145`: Backport PR #29144 on branch v3.10.x (Use both TCL_SETVAR and TCL_SETVAR2 for tcl 9 support) +* :ghpull:`29144`: Use both TCL_SETVAR and TCL_SETVAR2 for tcl 9 support +* :ghpull:`29140`: Backport PR #29080 on branch v3.10.x (Updates the ``galleries/tutorials/artists.py`` file in response to issue #28920) +* :ghpull:`29080`: Updates the ``galleries/tutorials/artists.py`` file in response to issue #28920 +* :ghpull:`29138`: Backport PR #29134 on branch v3.10.x (MNT: Temporarily skip failing test to unbreak CI) +* :ghpull:`29134`: MNT: Temporarily skip failing test to unbreak CI +* :ghpull:`29132`: Backport PR #29128 on branch v3.10.x (Tweak AutoMinorLocator docstring.) +* :ghpull:`29128`: Tweak AutoMinorLocator docstring. +* :ghpull:`29123`: Bump the actions group with 2 updates +* :ghpull:`29122`: Backport PR #29120 on branch v3.10.x (DOC: Switch nested pie example from cmaps to color_sequences) +* :ghpull:`29100`: Backport PR #29099 on branch v3.10.x (MNT: remove _ttconv.pyi) +* :ghpull:`29099`: MNT: remove _ttconv.pyi +* :ghpull:`29098`: Backport PR #29097 on branch v3.10.x (ENH: add back/forward buttons to osx backend move) +* :ghpull:`29097`: ENH: add back/forward buttons to osx backend move +* :ghpull:`29095`: Backport PR #29071 on branch v3.10.x (Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 in the actions group) +* :ghpull:`29096`: Backport PR #29094 on branch v3.10.x (DOC: fix link in See Also section of axes.violin) +* :ghpull:`29092`: Backport PR #29088 on branch v3.10.x (DOC: Format aliases in kwargs tables) +* :ghpull:`29094`: DOC: fix link in See Also section of axes.violin +* :ghpull:`29091`: Backport PR #29085 on branch v3.10.x (FIX: Update GTK3Agg backend export name for consistency) +* :ghpull:`29088`: DOC: Format aliases in kwargs tables +* :ghpull:`29089`: Backport PR #29065 on branch v3.10.x (DOC: Update docstring of triplot()) +* :ghpull:`29085`: FIX: Update GTK3Agg backend export name for consistency +* :ghpull:`29084`: Backport PR #29081 on branch v3.10.x (Document "none" as color value) +* :ghpull:`29065`: DOC: Update docstring of triplot() +* :ghpull:`29081`: Document "none" as color value +* :ghpull:`29061`: Backport PR #29024 on branch v3.10.x (Fix saving animations to transparent formats) +* :ghpull:`29069`: Backport PR #29068 on branch v3.10.x ([DOC] Fix indentation in sync_cmaps example) +* :ghpull:`29070`: Backport PR #29048 on branch v3.10.x (DOC: integrated pr workflow from contributing guide into install and workflow) +* :ghpull:`29048`: DOC: integrated pr workflow from contributing guide into install and workflow +* :ghpull:`29068`: [DOC] Fix indentation in sync_cmaps example +* :ghpull:`29024`: Fix saving animations to transparent formats +* :ghpull:`29059`: Cleanup converter docs and StrCategoryConverter behavior +* :ghpull:`29058`: [DOC] Update missing-references.json +* :ghpull:`29057`: DOC/TST: lock numpy<2.1 in environment.yml +* :ghpull:`29053`: Factor out common formats strings in LogFormatter, LogFormatterExponent. +* :ghpull:`28970`: Add explicit converter setting to Axis +* :ghpull:`28048`: Enables setting hatch linewidth in Patches and Collections, also fixes setting hatch linewidth by rcParams +* :ghpull:`29017`: DOC: Document preferred figure size for examples +* :ghpull:`28871`: updated contribution doc #28476 +* :ghpull:`28453`: Stop relying on dead-reckoning mouse buttons for motion_notify_event. +* :ghpull:`28495`: ticker.EngFormatter: allow offset +* :ghpull:`29039`: MNT: Add provisional get_backend(resolve=False) flag +* :ghpull:`28946`: MNT: Deprecate plt.polar() with an existing non-polar Axes +* :ghpull:`29013`: FIX: auto_fmtxdate for constrained layout +* :ghpull:`29022`: Fixes AIX internal CI build break. +* :ghpull:`28830`: Feature: Support passing DataFrames to table.table +* :ghpull:`27766`: Return filename from save_figure +* :ghpull:`27167`: ENH: add long_axis property to colorbar +* :ghpull:`29021`: Update minimum pybind11 to 2.13.2 +* :ghpull:`28863`: Improved documentation for quiver +* :ghpull:`29019`: Update requirements to add PyStemmer to doc-requirements and environment +* :ghpull:`28653`: Mnt/generalize plot varargs +* :ghpull:`28967`: Fix MSVC cast warnings +* :ghpull:`29016`: DOC: Better explain suptitle / supxlabel / supylabel naming +* :ghpull:`28842`: FT2Font extension improvements +* :ghpull:`28658`: New data → color pipeline +* :ghpull:`29012`: Bump required pybind11 to 2.13 +* :ghpull:`29007`: MNT: Deprecate changing Figure.number +* :ghpull:`28861`: Break Artist._remove_method reference cycle +* :ghpull:`28478`: bugfix for ``PathSimplifier`` +* :ghpull:`28992`: DOC: Refresh transform tree example +* :ghpull:`28890`: MNT: Add missing dependency to environment.yml +* :ghpull:`28354`: Add Quiverkey zorder option +* :ghpull:`28966`: Fix polar error bar cap orientation +* :ghpull:`28819`: Mark all extensions as free-threading safe +* :ghpull:`28986`: DOC: Add tags for 3D fill_between examples +* :ghpull:`28984`: DOC / BUG: Better example for 3D axlim_clip argument +* :ghpull:`20866`: Remove ttconv and implement Type-42 embedding using fontTools +* :ghpull:`28975`: Set guiEvent where applicable for gtk4. +* :ghpull:`28568`: added tags to mplot3d examples +* :ghpull:`28976`: Bump pypa/cibuildwheel from 2.21.2 to 2.21.3 in the actions group +* :ghpull:`28978`: CI: Resolve mypy stubtest build errors +* :ghpull:`28823`: Fix 3D rotation precession +* :ghpull:`28841`: Make mplot3d mouse rotation style adjustable +* :ghpull:`28971`: DOC: correct linestyle example and reference rcParams +* :ghpull:`28702`: [MNT]: #28701 separate the generation of polygon vertices in fill_between to enable resampling +* :ghpull:`28965`: Suggest imageio_ffmpeg to provide ffmpeg as animation writer. +* :ghpull:`28964`: FIX macos: Use the agg buffer_rgba rather than private attribute +* :ghpull:`28963`: Remove refs to outdated writers in animation.py. +* :ghpull:`28948`: Raise ValueError for RGB values outside the [0, 1] range in rgb_to_hsv function +* :ghpull:`28857`: Pybind11 cleanup +* :ghpull:`28949`: [pre-commit.ci] pre-commit autoupdate +* :ghpull:`28950`: Bump the actions group with 2 updates +* :ghpull:`28904`: Agg: Remove 16-bit limits +* :ghpull:`28856`: Convert remaining code to pybind11 +* :ghpull:`28874`: Remove remaining 3.8 deprecations +* :ghpull:`28943`: DOC: Clarify the returned line of axhline()/axvline() +* :ghpull:`28935`: DOC: Fix invalid rcParam references +* :ghpull:`28942`: In colorbar docs, add ref from 'boundaries' doc to 'spacing' doc. +* :ghpull:`28933`: Switch AxLine.set_xy{1,2} to take a single argument. +* :ghpull:`28869`: ci: Bump build image on AppVeyor to MSVC 2019 +* :ghpull:`28906`: Re-fix exception caching in dviread. +* :ghpull:`27349`: [ENH] Implement dynamic clipping to axes limits for 3D plots +* :ghpull:`28913`: DOC: Fix Axis.set_label reference +* :ghpull:`28911`: MNT: Fix double evaluation of _LazyTickList +* :ghpull:`28584`: MNT: Prevent users from erroneously using legend label API on Axis +* :ghpull:`28853`: MNT: Check the input sizes of regular X,Y in pcolorfast +* :ghpull:`28838`: TST: Fix minor issues in interactive backend test +* :ghpull:`28795`: MNT: Cleanup docstring substitution mechanisms +* :ghpull:`28897`: Fix minor issues in stubtest wrapper +* :ghpull:`28899`: Don't cache exception with traceback reference loop in dviread. +* :ghpull:`28888`: DOC: Better visualization for the default color cycle example +* :ghpull:`28896`: doc: specify non-python dependencies in dev install docs +* :ghpull:`28843`: MNT: Cleanup FontProperties __init__ API +* :ghpull:`28683`: MNT: Warn if fixed aspect overwrites explicitly set data limits +* :ghpull:`25645`: Fix issue with sketch not working on PathCollection in Agg +* :ghpull:`28886`: DOC: Cross-link Axes attributes +* :ghpull:`28880`: Remove 'in' from removal substitution for deprecation messages +* :ghpull:`28875`: DOC: Fix documentation of hist() kwarg lists +* :ghpull:`28825`: DOC: Fix non-working code object references +* :ghpull:`28862`: Improve pie chart error messages +* :ghpull:`28844`: DOC: Add illustration to Figure.subplots_adjust +* :ghpull:`28588`: Fix scaling in Tk on non-Windows systems +* :ghpull:`28849`: DOC: Mark subfigures as no longer provisional +* :ghpull:`26000`: making onselect a keyword argument on selectors +* :ghpull:`26013`: Support unhashable callbacks in CallbackRegistry +* :ghpull:`27011`: Convert Agg extension to pybind11 +* :ghpull:`28845`: In examples, prefer named locations rather than location numbers. +* :ghpull:`27218`: API: finish LocationEvent.lastevent removal +* :ghpull:`26870`: Removed the deprecated code from axis.py +* :ghpull:`27996`: Create ``InsetIndicator`` artist +* :ghpull:`28532`: TYP: Fix xycoords and friends +* :ghpull:`28785`: Convert ft2font extension to pybind11 +* :ghpull:`28815`: DOC: Document policy on colormaps and styles +* :ghpull:`28826`: MNT: Replace _docstring.dedent_interpd by its alias _docstring.interpd +* :ghpull:`27567`: DOC: batch of tags +* :ghpull:`27302`: Tags for simple_scatter.py demo +* :ghpull:`28820`: DOC: Fix missing cross-reference checks for sphinx-tags +* :ghpull:`28786`: Handle single color in ContourSet +* :ghpull:`28808`: DOC: Add a plot to margins() to visualize the effect +* :ghpull:`27938`: feat: add dunder method for math operations on Axes Size divider +* :ghpull:`28569`: Adding tags to many examples +* :ghpull:`28183`: Expire deprecations +* :ghpull:`28801`: DOC: Clarify AxLine.set_xy2 / AxLine.set_slope +* :ghpull:`28788`: TST: Skip webp tests if it isn't available +* :ghpull:`28550`: Remove internal use of ``Artist.figure`` +* :ghpull:`28767`: MNT: expire ``ContourSet`` deprecations +* :ghpull:`28755`: TYP: Add typing for internal _tri extension +* :ghpull:`28765`: Add tests for most of FT2Font, and fix some bugs +* :ghpull:`28781`: TST: Fix test_pickle_load_from_subprocess in a dirty tree +* :ghpull:`28783`: Fix places where "auto" was not listed as valid interpolation_stage. +* :ghpull:`28779`: DOC/TST: lock numpy < 2.1 +* :ghpull:`28771`: Ensure SketchParams is always fully initialized +* :ghpull:`28375`: FIX: Made AffineDeltaTransform pass-through properly +* :ghpull:`28454`: MultivarColormap and BivarColormap +* :ghpull:`27891`: Refactor some parts of ft2font extension +* :ghpull:`28752`: quick fix dev build by locking out numpy version that's breaking things +* :ghpull:`28749`: Add sphinxcontrib-video to environment.yml +* :ghpull:`27851`: Add ten-color accessible color cycle as style sheet +* :ghpull:`28501`: ConciseDateFormatter's offset string is correct on an inverted axis +* :ghpull:`28734`: Compressed layout moves suptitle +* :ghpull:`28736`: Simplify some code in dviread +* :ghpull:`28347`: Doc: added triage section to new contributor docs +* :ghpull:`28735`: ci: Avoid setuptools 72.2.0 when installing kiwi on PyPy +* :ghpull:`28728`: MNT: Deprecate reimported functions in top-level namespace +* :ghpull:`28730`: MNT: Don't rely on RcParams being a dict subclass in internal code +* :ghpull:`28714`: Simplify _api.warn_external on Python 3.12+ +* :ghpull:`28727`: MNT: Better workaround for format_cursor_data on ScalarMappables +* :ghpull:`28725`: Stop disabling FH4 Exception Handling on MSVC +* :ghpull:`28711`: Merge branch v3.9.x into main +* :ghpull:`28713`: DOC: Add a few more notes to release guide +* :ghpull:`28720`: DOC: Clarify axhline() uses axes coordinates +* :ghpull:`28718`: DOC: Update missing references for numpydoc 1.8.0 +* :ghpull:`28710`: DOC: clarify alpha handling for indicate_inset[_zoom] +* :ghpull:`28704`: Fixed arrowstyle doc interpolation in FancyPatch.set_arrow() #28698. +* :ghpull:`28709`: Bump actions/attest-build-provenance from 1.4.0 to 1.4.1 in the actions group +* :ghpull:`28707`: Avoid division-by-zero in Sketch::Sketch +* :ghpull:`28610`: CI: Add CI to test matplotlib against free-threaded Python +* :ghpull:`28262`: Fix PolygonSelector cursor to temporarily hide during active zoom/pan +* :ghpull:`28670`: API: deprecate unused helper in patch._Styles +* :ghpull:`28589`: Qt embedding example: Separate drawing and data retrieval timers +* :ghpull:`28655`: Inline annotation and PGF user demos +* :ghpull:`28654`: DOC: Remove long uninstructive examples +* :ghpull:`28652`: Fix docstring style inconsistencies in lines.py +* :ghpull:`28641`: DOC: Standardize example titles - part 2 +* :ghpull:`28642`: DOC: Simplify heatmap example +* :ghpull:`28638`: DOC: Remove hint on PRs from origin/main +* :ghpull:`28587`: Added dark-mode diverging colormaps +* :ghpull:`28546`: DOC: Clarify/simplify example of multiple images with one colorbar +* :ghpull:`28613`: Added documentation for parameters vmin and vmax inside specgram function. +* :ghpull:`28627`: DOC: Bump minimum Sphinx to 5.1.0 +* :ghpull:`28628`: DOC: Sub-structure next API changes overview +* :ghpull:`28629`: FIX: ``Axis.set_in_layout`` respected +* :ghpull:`28575`: Add branch tracking to development workflow instructions +* :ghpull:`28616`: CI: Build docs on latest Python +* :ghpull:`28617`: DOC: Enable parallel builds +* :ghpull:`28544`: DOC: Standardize example titles +* :ghpull:`28615`: DOC: hack to suppress sphinx-gallery 17.0 warning +* :ghpull:`28293`: BLD: Enable building Python 3.13 wheels for nightlies +* :ghpull:`27385`: Fix 3D lines being visible when behind camera +* :ghpull:`28609`: svg: Ensure marker-only lines get URLs +* :ghpull:`28599`: Upgrade code to Python 3.10 +* :ghpull:`28593`: Update ruff to 0.2.0 +* :ghpull:`28603`: Simplify ttconv python<->C++ conversion using std::optional. +* :ghpull:`28557`: DOC: apply toc styling to remove nesting +* :ghpull:`28542`: CI: adjust pins in mypy GHA job +* :ghpull:`28504`: Changes in SVG backend to improve compatibility with Affinity designer +* :ghpull:`28122`: Disable clipping in Agg resamplers. +* :ghpull:`28597`: Pin PyQt6 back on Ubuntu 20.04 +* :ghpull:`28073`: Add support for multiple hatches, edgecolors and linewidths in histograms +* :ghpull:`28594`: MNT: Raise on GeoAxes limits manipulation +* :ghpull:`28312`: Remove one indirection layer in ToolSetCursor. +* :ghpull:`28573`: ENH: include property name in artist AttributeError +* :ghpull:`28503`: Bump minimum Python to 3.10 +* :ghpull:`28525`: FIX: colorbar pad for ``ImageGrid`` +* :ghpull:`28558`: DOC: Change _make_image signature to numpydoc +* :ghpull:`28061`: API: add antialiased to interpolation-stage in image +* :ghpull:`28536`: [svg] Add rcParam["svg.id"] to add a top-level id attribute to +* :ghpull:`28540`: Subfigures become stale when their artists are stale +* :ghpull:`28177`: Rationalise artist get_figure methods; make figure attribute a property +* :ghpull:`28527`: DOC: improve tagging guidelines page +* :ghpull:`28530`: DOC: Simplify axhspan example +* :ghpull:`28537`: DOC: Update timeline example for newer releases +* :ghpull:`27833`: [SVG] Introduce sequential ID-generation scheme for clip-paths. +* :ghpull:`28512`: DOC: Fix version switcher for stable docs +* :ghpull:`28492`: MNT: Remove PolyQuadMesh deprecations +* :ghpull:`28509`: CI: Use micromamba on AppVeyor +* :ghpull:`28510`: Merge v3.9.1 release into main +* :ghpull:`28494`: [pre-commit.ci] pre-commit autoupdate +* :ghpull:`28497`: Add words to ignore for codespell +* :ghpull:`28455`: Expand ticklabels_rotation example to cover rotating default ticklabels. +* :ghpull:`28282`: DOC: clarify no-build-isolation & mypy ignoring new functions +* :ghpull:`28306`: Fixed PolarAxes not using fmt_xdata and added simple test (#4568) +* :ghpull:`28400`: DOC: Improve doc wording of data parameter +* :ghpull:`28225`: [ENH]: fill_between extended to 3D +* :ghpull:`28371`: Bump pypa/cibuildwheel from 2.18.1 to 2.19.0 in the actions group +* :ghpull:`28390`: Inline RendererBase._get_text_path_transform. +* :ghpull:`28381`: Take hinting rcParam into account in MathTextParser cache. +* :ghpull:`28363`: flip subfigures axes to match subplots +* :ghpull:`28340`: Fix missing font error when using MiKTeX +* :ghpull:`28379`: PathEffectsRenderer can plainly inherit RendererBase._draw_text_as_path. +* :ghpull:`28275`: Revive sanitizing default filenames extracted from UI window titles +* :ghpull:`28360`: DOC: fixed code for testing check figures equal example +* :ghpull:`28370`: Reorder Axes3D parameters semantically. +* :ghpull:`28350`: Typo in communication guide: extensiblity -> extensibility +* :ghpull:`28290`: Introduce natural 3D rotation with mouse +* :ghpull:`28186`: apply unary minus spacing directly after equals sign +* :ghpull:`28311`: Update 3D orientation indication right away +* :ghpull:`28300`: Faster title alignment +* :ghpull:`28313`: Factor out handling of missing spines in alignment calculations. +* :ghpull:`28196`: TST: add timeouts to font_manager + threading test +* :ghpull:`28279`: Doc/ipython dep +* :ghpull:`28091`: [MNT]: create build-requirements.txt and update dev-requirements.txt +* :ghpull:`27992`: Add warning for multiple pyplot.figure calls with same ID +* :ghpull:`28238`: DOC: Update release guide to match current automations +* :ghpull:`28232`: Merge v3.9.0 release into main +* :ghpull:`28228`: DOC: Fix typo in release_guide.rst +* :ghpull:`28074`: Add ``orientation`` parameter to Boxplot and deprecate ``vert`` +* :ghpull:`27998`: Add a new ``orientation`` parameter to Violinplot and deprecate ``vert`` +* :ghpull:`28217`: Better group logging of font handling by texmanager. +* :ghpull:`28130`: Clarify the role of out_mask and out_alpha in _make_image. +* :ghpull:`28201`: Deprecate ``Poly3DCollection.get_vector`` +* :ghpull:`28046`: DOC: Clarify merge policy +* :ghpull:`26893`: PGF: Consistently set LaTeX document font size +* :ghpull:`28156`: Don't set savefig.facecolor/edgecolor in dark_background/538 styles. +* :ghpull:`28030`: Fix #28016: wrong lower ylim when baseline=None on stairs +* :ghpull:`28127`: GOV: write up policy on not updating req for CVEs in dependencies +* :ghpull:`28106`: Fix: [Bug]: Setting norm by string doesn't work for hexbin #28105 +* :ghpull:`28143`: Merge branch v3.9.x into main +* :ghpull:`28133`: Make ``functions`` param to secondary_x/yaxis not keyword-only. +* :ghpull:`28083`: Convert TensorFlow to numpy for plots +* :ghpull:`28116`: FIX: Correct names of aliased cmaps +* :ghpull:`28118`: Remove redundant baseline tests in test_image. +* :ghpull:`28093`: Minor maintenance on pgf docs/backends. +* :ghpull:`27818`: Set polygon offsets for log scaled hexbin +* :ghpull:`28058`: TYP: add float to to_rgba x type +* :ghpull:`27964`: BUG: Fix NonUniformImage with nonlinear scale +* :ghpull:`28054`: DOC: Clarify that parameters to gridded data plotting functions are p… +* :ghpull:`27882`: Deleting all images that have passed tests before upload +* :ghpull:`28033`: API: warn if stairs used in way that is likely not desired +* :ghpull:`27786`: Deprecate positional use of most arguments of plotting functions +* :ghpull:`28025`: DOC: Clarify interface terminology +* :ghpull:`28043`: MNT: Add git blame ignore for docstring parameter indentation fix +* :ghpull:`28037`: DOC: Fix inconsistent spacing in some docstrings in _axes.py +* :ghpull:`28031`: Be more specific in findobj return type -Issues (6): +Issues (100): -* :ghissue:`28960`: [Bug]: High CPU utilization of the macosx backend -* :ghissue:`28990`: [Bug]: no longer able to set multiple hatch colors -* :ghissue:`28870`: [Bug]: axline doesn't work with some axes scales -* :ghissue:`28386`: [Bug]: Minor issue - Drawing an axline sets slopes less than 1E-8 to 0 -* :ghissue:`28817`: [Bug]: ``~/.config/matplotlib`` is never used because ``~/.config`` is a symlink -* :ghissue:`28716`: Size hint method in Qt backend should be named ``minimumSizeHint``, not ``minumumSizeHint`` +* :ghissue:`29298`: [Doc]: The link at "see also" is incorrect. (Axes.violin) +* :ghissue:`29248`: [Bug]: Figure.align_labels() confused by GridSpecFromSubplotSpec +* :ghissue:`26738`: Improve LineCollection docstring further +* :ghissue:`29263`: [Bug]: mypy failures in CI +* :ghissue:`27416`: [Bug]: get_tick_params on xaxis shows wrong keywords +* :ghissue:`29241`: [Bug]: Instructions for setting up conda dev environment in environment.yml give issues with MacOS/zsh +* :ghissue:`29227`: [Bug]: Introductory example on the pyplot API page does not show - missing plt.show() +* :ghissue:`29190`: [Bug]: inconsistent ‘animation.FuncAnimation’ between display and save +* :ghissue:`29090`: [MNT]: More consistent color parameters for bar() +* :ghissue:`29179`: [Bug]: Incorrect pcolormesh when shading='nearest' and only the mesh data C is provided. +* :ghissue:`29067`: [Bug]: ``secondary_xaxis`` produces ticks at incorrect locations +* :ghissue:`29126`: [Bug]: TkAgg backend is broken with tcl/tk 9.0 +* :ghissue:`29045`: [ENH]: implement back/forward buttons on mouse move events on macOS +* :ghissue:`27173`: [Bug]: Gifs no longer create transparent background +* :ghissue:`19229`: Add public API for setting an axis unit converter +* :ghissue:`21108`: [Bug]: Hatch linewidths cannot be modified in an rcParam context +* :ghissue:`27784`: [Bug]: Polar plot error bars don't rotate with angle for ``set_theta_direction`` and ``set_theta_offset`` +* :ghissue:`29011`: [Bug]: Figure.autofmt_xdate() not working in presence of colorbar with constrained layout +* :ghissue:`29020`: AIX internal CI build break #Matplotlib +* :ghissue:`28726`: feature request: support passing DataFrames to table.table +* :ghissue:`28570`: [MNT]: Try improving doc build speed by using PyStemmer +* :ghissue:`13388`: Typo in the figure API (fig.suptitle) +* :ghissue:`28994`: [Bug]: Figure Number Gives Type Error +* :ghissue:`28985`: [ENH]: Cannot disable coordinate display in ToolManager/Toolbar (it's doable in NavigationToolbar2) +* :ghissue:`17914`: ``PathSimplifier`` fails to ignore ``CLOSEPOLY`` vertices +* :ghissue:`28885`: [Bug]: Strange errorbar caps when polar axes have non-default theta direction or theta zero location +* :ghissue:`12418`: replace ttconv for ps/pdf +* :ghissue:`28962`: [Bug]: gtk4 backend does not set guiEvent attribute +* :ghissue:`28408`: [ENH]: mplot3d mouse rotation style +* :ghissue:`28701`: [MNT]: Separate the generation of polygon vertices from ``_fill_between_x_or_y`` +* :ghissue:`28941`: [Bug]: unexplicit error message when using ``matplotlib.colors.rgb_to_hsv()`` with wrong input +* :ghissue:`23846`: [MNT]: Pybind11 transition plan +* :ghissue:`28866`: Possible memory leak in pybind11 migration +* :ghissue:`26368`: [Bug]: Long audio files result in incomplete spectrogram visualizations +* :ghissue:`23826`: [Bug]: Overflow of 16-bit integer in Agg renderer causes PolyCollections to be drawn at incorrect locations +* :ghissue:`28927`: [Bug]: Enforce that Line data modifications are sequences +* :ghissue:`12312`: colorbar(boundaries=...) doesn't work so well with nonlinear norms +* :ghissue:`28800`: [ENH]: AxLine xy1/xy2 setters should take xy as single parameters, (possibly) not separate ones +* :ghissue:`28893`: [Bug]: Lines between points are invisible when there are more than 7 subfigures per row +* :ghissue:`28908`: [Bug]: Possible performance issue with _LazyTickList +* :ghissue:`27971`: [Bug]: ax.xaxis.set_label(...) doesn't set the x-axis label +* :ghissue:`28059`: [Bug]: pcolorfast should validate that regularly spaced X or Y inputs have the right size +* :ghissue:`28892`: [Doc]: Be more specific on dependencies that need to be installed for a "reasonable" dev environment +* :ghissue:`19693`: path.sketch doesn't apply to PolyCollection +* :ghissue:`28873`: [Bug]: hist()'s doc for edgecolors/facecolors does not match behavior (which is itself not very consistent) +* :ghissue:`23005`: [Doc]: Add figure to ``subplots_adjust`` +* :ghissue:`25947`: [Doc]: Subfigures still marked as provisional +* :ghissue:`26012`: [Bug]: "Unhashable type" when event callback is a method of a ``dict`` subclass +* :ghissue:`23425`: [Bug]: Axes.indicate_inset connectors affect constrained layout +* :ghissue:`23424`: [Bug]: Axes.indicate_inset(linewidth=...) doesn't affect connectors +* :ghissue:`19768`: Overlay created by ``Axes.indicate_inset_zoom`` does not adjust when changing inset ranges +* :ghissue:`27673`: [Doc]: Confusing page on color changes +* :ghissue:`28782`: [Bug]: String ``contour(colors)`` gives confusing error when ``extend`` used +* :ghissue:`27930`: [ENH]: Make axes_grid1.Size more math friendly. +* :ghissue:`28372`: [Bug]: AffineDeltaTransform does not appear to invalidate properly +* :ghissue:`27866`: [Bug]: Adding suptitle in compressed layout causes weird spacing +* :ghissue:`28731`: [Bug]: Plotting numpy.array of dtype float32 with pyplot.imshow and specified colors.LogNorm produces wrong colors +* :ghissue:`28715`: [Bug]: CI doc builds fail since a couple of days +* :ghissue:`28698`: [bug]: arrowstyle doc interpolation in FancyPatch.set_arrow() +* :ghissue:`28669`: [Bug]: division-by-zero error in Sketch::Sketch with Agg backend +* :ghissue:`28548`: [Doc]: matplotlib.pyplot.specgram parameters vmin and vmax are not documented +* :ghissue:`28165`: [Bug]: PolygonSelector should hide itself when zoom/pan is active +* :ghissue:`18608`: Feature proposal: "Dark mode" divergent colormaps +* :ghissue:`28623`: [Bug]: ``Axis.set_in_layout`` not respected? +* :ghissue:`6305`: Matplotlib 3D plot - parametric curve “wraparound” from certain perspectives +* :ghissue:`28595`: [Bug]: set_url without effect for instances of Line2D with linestyle 'none' +* :ghissue:`20910`: [Bug]: Exported SVG files are no longer imported Affinity Designer correctly +* :ghissue:`28600`: [TST] Upcoming dependency test failures +* :ghissue:`26718`: [Bug]: stacked histogram does not properly handle edgecolor and hatches +* :ghissue:`28590`: [ENH]: Geo Projections support for inverting axis +* :ghissue:`27954`: [ENH]: Iterables in grouped histogram labels +* :ghissue:`27878`: [ENH]: AttributeError('... got an unexpected keyword argument ...') should set the .name attribute to the keyword +* :ghissue:`28489`: [TST] Upcoming dependency test failures +* :ghissue:`28343`: [Bug]: inconsistent colorbar pad for ``ImageGrid`` with ``cbar_mode="single"`` +* :ghissue:`28535`: [ENH]: Add id attribute to top level svg tag +* :ghissue:`28170`: [Doc]: ``get_figure`` may return a ``SubFigure`` +* :ghissue:`27831`: [Bug]: Nondeterminism in SVG clipPath element id attributes +* :ghissue:`4568`: Add ``fmt_r`` and ``fmt_theta`` methods to polar axes +* :ghissue:`28105`: [Bug]: Setting norm by string doesn't work for hexbin +* :ghissue:`28142`: [ENH]: Add fill between support for 3D plots +* :ghissue:`28344`: [Bug]: subfigures are added in column major order +* :ghissue:`28212`: [Bug]: Matplotlib not work with MiKTeX. +* :ghissue:`28288`: [ENH]: Natural 3D rotation with mouse +* :ghissue:`28180`: [Bug]: mathtext should distinguish between unary and binary minus +* :ghissue:`26150`: [Bug]: Savefig slow with subplots +* :ghissue:`28310`: [Bug]: orientation indication shows up late in mplot3d, and then lingers +* :ghissue:`16263`: Apply NEP29 (time-limited support) to IPython +* :ghissue:`28192`: [MNT]: Essential build requirements not included in dev-requirements +* :ghissue:`27978`: [Bug]: strange behaviour when redefining figure size +* :ghissue:`13435`: boxplot/violinplot orientation-setting API +* :ghissue:`28199`: [MNT]: Misleading function name ``Poly3DCollection.get_vector()`` +* :ghissue:`26892`: [Bug]: PGF font size mismatch between measurement and output +* :ghissue:`28016`: [Bug]: Unexpected ylim of stairs with baseline=None +* :ghissue:`28114`: [Bug]: mpl.colormaps[ "Grays" ].name is "Greys", not "Grays" +* :ghissue:`18045`: Cannot access hexbin data when ``xscale='log'`` and ``yscale='log'`` are set. +* :ghissue:`27820`: [Bug]: Logscale Axis + NonUniformImage + GUI move tool = Distortion +* :ghissue:`28047`: [Bug]: plt.barbs is a command that cannot be passed in a c parameter by parameter name, but can be passed in the form of a positional parameter +* :ghissue:`23400`: Only upload failed images on failure +* :ghissue:`26752`: [Bug]: ``ax.stairs()`` creates inaccurate ``fill`` for the plot +* :ghissue:`21817`: [Doc/Dev]: style guide claims "object oriented" is verboten. Previous GitHub statistics diff --git a/doc/users/next_whats_new/README.rst b/doc/users/next_whats_new/README.rst index 98b601ee32d8..362feda65271 100644 --- a/doc/users/next_whats_new/README.rst +++ b/doc/users/next_whats_new/README.rst @@ -1,9 +1,19 @@ :orphan: +.. NOTE TO EDITORS OF THIS FILE + This file serves as the README directly available in the file system next to the + next_whats_new entries. The content between the ``whats-new-guide-*`` markers is + additionally included in the documentation page ``doc/devel/api_changes.rst``. Please + check that the page builds correctly after changing this file. + + Instructions for writing "What's new" entries ============================================= -Please place new portions of `whats_new.rst` in the `next_whats_new` directory. +.. whats-new-guide-start + +Please place new portions of :file:`whats_new.rst` in the +:file:`doc/users/next_whats_new/` directory. When adding an entry please look at the currently existing files to see if you can extend any of them. If you create a file, name it @@ -26,6 +36,4 @@ Please avoid using references in section titles, as it causes links to be confusing in the table of contents. Instead, ensure that a reference is included in the descriptive text. -.. NOTE - Lines 5-24 of this file are include in :ref:`api_whats_new`; - therefore, please check the doc build after changing this file. +.. whats-new-guide-end diff --git a/doc/users/prev_whats_new/github_stats_3.9.0.rst b/doc/users/prev_whats_new/github_stats_3.9.0.rst index b1d229ffbfa1..5ddbdfd6f2bd 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.0.rst +++ b/doc/users/prev_whats_new/github_stats_3.9.0.rst @@ -1,4 +1,4 @@ -.. _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.3.rst b/doc/users/prev_whats_new/github_stats_3.9.3.rst new file mode 100644 index 000000000000..06f0232c338c --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.9.3.rst @@ -0,0 +1,108 @@ +.. _github-stats-3-9-3: + +GitHub statistics for 3.9.3 (Nov 30, 2024) +========================================== + +GitHub statistics for 2024/08/12 (tag: v3.9.2) - 2024/11/30 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 6 issues and merged 62 pull requests. +The full list can be seen `on GitHub `__ + +The following 18 authors contributed 90 commits. + +* Andresporcruz +* Antony Lee +* Charlie LeWarne +* dependabot[bot] +* Elliott Sales de Andrade +* Gavin S +* Greg Lucas +* hannah +* Kyle Sunden +* Kyra Cho +* kyracho +* Lumberbot (aka Jack) +* Michael Hinton +* Oscar Gustafsson +* Ruth Comer +* Thomas A Caswell +* Tim Hoffmann +* vittoboa + +GitHub issues and pull requests: + +Pull Requests (62): + +* :ghpull:`29195`: Backport PR #29191 on branch v3.9.x (ci: Simplify 3.13t test setup) +* :ghpull:`29191`: ci: Simplify 3.13t test setup +* :ghpull:`29176`: Backport PR #29148 on branch v3.9.x (Don't fail on equal-but-differently-named cmaps in qt figureoptions.) +* :ghpull:`29148`: Don't fail on equal-but-differently-named cmaps in qt figureoptions. +* :ghpull:`29165`: Backport PR #29153 on branch v3.9.x (Bump codecov/codecov-action from 4 to 5 in the actions group) +* :ghpull:`29153`: Bump codecov/codecov-action from 4 to 5 in the actions group +* :ghpull:`29149`: Backport CI config updates to v3.9.x +* :ghpull:`29121`: Backport PR #29120 on branch v3.9.x (DOC: Switch nested pie example from cmaps to color_sequences) +* :ghpull:`29071`: Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 in the actions group +* :ghpull:`29046`: Backport PR #28981 on branch v3.9.x (FIX: macos: Use standard NSApp run loop in our input hook) +* :ghpull:`28981`: FIX: macos: Use standard NSApp run loop in our input hook +* :ghpull:`29041`: Backport PR #29035 on branch v3.9.x (FIX: Don't set_wmclass on GTK3) +* :ghpull:`29035`: FIX: Don't set_wmclass on GTK3 +* :ghpull:`29037`: Backport PR #29036 on branch v3.9.x (Don't pass redundant inline=True to example clabel() calls.) +* :ghpull:`29032`: Backport PR #27569 on branch v3.9.x (DOC: initial tags for statistics section of gallery) +* :ghpull:`29034`: Backport PR #29031 on branch v3.9.x (DOC: Fix copy-paste typo in ColorSequenceRegistry) +* :ghpull:`29031`: DOC: Fix copy-paste typo in ColorSequenceRegistry +* :ghpull:`29015`: Backport PR #29014 on branch v3.9.x (FIX: fake out setuptools scm in tox on ci) +* :ghpull:`29014`: FIX: fake out setuptools scm in tox on ci +* :ghpull:`29010`: Backport PR #29005 on branch v3.9.x (DOC: Update meson-python intersphinx link) +* :ghpull:`29006`: Backport PR #28993 on branch v3.9.x (FIX: contourf hatches use multiple edgecolors) +* :ghpull:`28993`: FIX: contourf hatches use multiple edgecolors +* :ghpull:`28988`: Backport PR #28987 on branch v3.9.x (Fix: Do not use numeric tolerances for axline special cases) +* :ghpull:`28947`: Backport PR #28925 on branch v3.9.x (TST: handle change in pytest.importorskip behavior) +* :ghpull:`28989`: Backport PR #28972 on branch v3.9.x (Switch macOS 12 runner images to macOS 13) +* :ghpull:`28972`: Switch macOS 12 runner images to macOS 13 +* :ghpull:`28987`: Fix: Do not use numeric tolerances for axline special cases +* :ghpull:`28954`: Backport PR #28952 on branch v3.9.x (BLD: update trove metadata to support py3.13) +* :ghpull:`28952`: BLD: update trove metadata to support py3.13 +* :ghpull:`28887`: Backport PR #28883 on branch v3.9.x (Only check X11 when running Tkinter tests) +* :ghpull:`28926`: Backport PR #28689 on branch v3.9.x (ci: Enable testing on Python 3.13) +* :ghpull:`28925`: TST: handle change in pytest.importorskip behavior +* :ghpull:`28945`: Backport PR #28943 on branch v3.9.x (DOC: Clarify the returned line of axhline()/axvline()) +* :ghpull:`28939`: Backport PR #28900 on branch v3.9.x (DOC: Improve fancybox demo) +* :ghpull:`28900`: DOC: Improve fancybox demo +* :ghpull:`28902`: Backport PR #28881 on branch v3.9.x (Fix ``axline`` for slopes <= 1E-8. Closes #28386) +* :ghpull:`28431`: Fix ``axline`` for slopes < 1E-8 +* :ghpull:`28881`: Fix ``axline`` for slopes <= 1E-8. Closes #28386 +* :ghpull:`28883`: Only check X11 when running Tkinter tests +* :ghpull:`28859`: Backport PR #28858 on branch v3.9.x (Fix flaky labelcolor tests) +* :ghpull:`28858`: Fix flaky labelcolor tests +* :ghpull:`28839`: Backport PR #28836 on branch v3.9.x (MNT: Use __init__ parameters of font properties) +* :ghpull:`28836`: MNT: Use __init__ parameters of font properties +* :ghpull:`28828`: Backport PR #28818 on branch v3.9.x (Resolve configdir so that it's not a symlink when is_dir() is called) +* :ghpull:`28818`: Resolve configdir so that it's not a symlink when is_dir() is called +* :ghpull:`28811`: Backport PR #28810 on branch v3.9.x (Document how to obtain sans-serif usetex math.) +* :ghpull:`28806`: Backport PR #28805 on branch v3.9.x (add brackets to satisfy the new sequence requirement) +* :ghpull:`28802`: Backport PR #28798 on branch v3.9.x (DOC: Correctly list modules that have been internalized) +* :ghpull:`28791`: Backport PR #28790 on branch v3.9.x (DOC: Fix duplicate Figure.set_dpi entry) +* :ghpull:`28787`: Backport PR #28706 on branch v3.9.x (Add Returns info to to_jshtml docstring) +* :ghpull:`28706`: Add Returns info to to_jshtml docstring +* :ghpull:`28751`: Backport PR #28271 on branch v3.9.x (Fix draggable legend disappearing when picking while use_blit=True) +* :ghpull:`28271`: Fix draggable legend disappearing when picking while use_blit=True +* :ghpull:`28747`: Backport PR #28743 on branch v3.9.x (Minor fixes in ticker docs) +* :ghpull:`28743`: Minor fixes in ticker docs +* :ghpull:`28738`: Backport PR #28737 on branch v3.9.x (TST: Fix image comparison directory for test_striped_lines) +* :ghpull:`28740`: Backport PR #28739 on branch v3.9.x (Tweak interactivity docs wording (and fix capitalization).) +* :ghpull:`28737`: TST: Fix image comparison directory for test_striped_lines +* :ghpull:`28733`: Backport PR #28732 on branch v3.9.x (Renames the minumumSizeHint method to minimumSizeHint) +* :ghpull:`28732`: Renames the minumumSizeHint method to minimumSizeHint +* :ghpull:`28689`: ci: Enable testing on Python 3.13 +* :ghpull:`28724`: Backport fixes from #28711 + +Issues (6): + +* :ghissue:`28960`: [Bug]: High CPU utilization of the macosx backend +* :ghissue:`28990`: [Bug]: no longer able to set multiple hatch colors +* :ghissue:`28870`: [Bug]: axline doesn't work with some axes scales +* :ghissue:`28386`: [Bug]: Minor issue - Drawing an axline sets slopes less than 1E-8 to 0 +* :ghissue:`28817`: [Bug]: ``~/.config/matplotlib`` is never used because ``~/.config`` is a symlink +* :ghissue:`28716`: Size hint method in Qt backend should be named ``minimumSizeHint``, not ``minumumSizeHint`` diff --git a/doc/users/prev_whats_new/github_stats_3.9.4.rst b/doc/users/prev_whats_new/github_stats_3.9.4.rst new file mode 100644 index 000000000000..a821d2fc1f57 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.9.4.rst @@ -0,0 +1,30 @@ +.. _github-stats-3-9-4: + +GitHub statistics for 3.9.4 (Dec 13, 2024) +========================================== + +GitHub statistics for 2024/11/30 (tag: v3.9.3) - 2024/12/13 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 2 issues and merged 3 pull requests. +The full list can be seen `on GitHub `__ + +The following 3 authors contributed 15 commits. + +* Elliott Sales de Andrade +* Scott Shambaugh +* Victor Liu + +GitHub issues and pull requests: + +Pull Requests (3): + +* :ghpull:`29297`: Backport PR #29295 on branch v3.9.x (BLD: Pin meson-python to <0.17.0) +* :ghpull:`29295`: BLD: Pin meson-python to <0.17.0 +* :ghpull:`29175`: addressing issue #29156, converted ps to array before slicing + +Issues (2): + +* :ghissue:`29229`: [Bug]: Icons do not work with GTK +* :ghissue:`29156`: [Bug]: Poly3DCollection initialization cannot properly handle parameter verts when it is a list of nested tuples and shade is False diff --git a/doc/users/prev_whats_new/whats_new_1.3.rst b/doc/users/prev_whats_new/whats_new_1.3.rst index 10811632c5c4..af40f37f92b7 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/users/prev_whats_new/whats_new_1.3.rst @@ -292,9 +292,9 @@ rcParam has been set, and will not retroactively affect already existing text objects. This brings their behavior in line with most other rcParams. -Added :rc:`savefig.jpeg_quality` -```````````````````````````````` -rcParam value :rc:`savefig.jpeg_quality` was added so that the user can +Added ``savefig.jpeg_quality`` rcParam +`````````````````````````````````````` +The ``savefig.jpeg_quality`` rcParam was added so that the user can configure the default quality used when a figure is written as a JPEG. The default quality is 95; previously, the default quality was 75. This change minimizes the artifacting inherent in JPEG images, diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/users/prev_whats_new/whats_new_1.5.rst index dd8e204aa957..5bb4d4b9b5e9 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -190,8 +190,8 @@ Some parameters have been added, others have been improved. +---------------------------+--------------------------------------------------+ | Parameter | Description | +===========================+==================================================+ -|:rc:`xaxis.labelpad`, | mplot3d now respects these parameters | -|:rc:`yaxis.labelpad` | | +|``xaxis.labelpad``, | mplot3d now respects these attributes, which | +|``yaxis.labelpad`` | default to :rc:`axes.labelpad` | +---------------------------+--------------------------------------------------+ |:rc:`axes.labelpad` | Default space between the axis and the label | +---------------------------+--------------------------------------------------+ @@ -368,11 +368,6 @@ kwargs names is not ideal, but `.Axes.fill_between` already has a This is particularly useful for plotting pre-binned histograms. -.. figure:: ../../gallery/lines_bars_and_markers/images/sphx_glr_filled_step_001.png - :target: ../../gallery/lines_bars_and_markers/filled_step.html - :align: center - :scale: 50 - Square Plot ``````````` diff --git a/doc/users/prev_whats_new/whats_new_3.1.0.rst b/doc/users/prev_whats_new/whats_new_3.1.0.rst index 3d63768f9c7a..9f53435b89f6 100644 --- a/doc/users/prev_whats_new/whats_new_3.1.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.1.0.rst @@ -260,8 +260,7 @@ Default minor tick spacing was changed from 0.625 to 0.5 for major ticks spaced A public API has been added to `.EngFormatter` to control how the numbers in the ticklabels will be rendered. By default, *useMathText* evaluates to -:rc:`axes.formatter.use_mathtext'` and *usetex* evaluates to -:rc:`'text.usetex'`. +:rc:`axes.formatter.use_mathtext` and *usetex* evaluates to :rc:`text.usetex`. If either is `True` then the numbers will be encapsulated by ``$`` signs. When using ``TeX`` this implies that the numbers will be shown diff --git a/doc/users/prev_whats_new/whats_new_3.10.0.rst b/doc/users/prev_whats_new/whats_new_3.10.0.rst new file mode 100644 index 000000000000..bc160787aebc --- /dev/null +++ b/doc/users/prev_whats_new/whats_new_3.10.0.rst @@ -0,0 +1,479 @@ +New more-accessible color cycle +------------------------------- + +A new color cycle named 'petroff10' was added. This cycle was constructed using a +combination of algorithmically-enforced accessibility constraints, including +color-vision-deficiency modeling, and a machine-learning-based aesthetics model +developed from a crowdsourced color-preference survey. It aims to be both +generally pleasing aesthetically and colorblind accessible such that it could +serve as a default in the aim of universal design. For more details +see `Petroff, M. A.: "Accessible Color Sequences for Data Visualization" +`_ and related `SciPy talk`_. A demonstration +is included in the style sheets reference_. To load this color cycle in place +of the default:: + + import matplotlib.pyplot as plt + plt.style.use('petroff10') + +.. _reference: https://matplotlib.org/gallery/style_sheets/style_sheets_reference.html +.. _SciPy talk: https://www.youtube.com/watch?v=Gapv8wR5DYU + +Dark-mode diverging colormaps +----------------------------- + +Three diverging colormaps have been added: "berlin", "managua", and "vanimo". +They are dark-mode diverging colormaps, with minimum lightness at the center, +and maximum at the extremes. These are taken from F. Crameri's Scientific +colour maps version 8.0.1 (DOI: https://doi.org/10.5281/zenodo.1243862). + + +.. plot:: + :include-source: true + :alt: Example figures using "imshow" with dark-mode diverging colormaps on positive and negative data. First panel: "berlin" (blue to red with a black center); second panel: "managua" (orange to cyan with a dark purple center); third panel: "vanimo" (pink to green with a black center). + + import numpy as np + import matplotlib.pyplot as plt + + vals = np.linspace(-5, 5, 100) + x, y = np.meshgrid(vals, vals) + img = np.sin(x*y) + + _, ax = plt.subplots(1, 3) + ax[0].imshow(img, cmap=plt.cm.berlin) + ax[1].imshow(img, cmap=plt.cm.managua) + ax[2].imshow(img, cmap=plt.cm.vanimo) + +Specifying a single color in ``contour`` and ``contourf`` +--------------------------------------------------------- + +`~.Axes.contour` and `~.Axes.contourf` previously accepted a single color +provided it was expressed as a string. This restriction has now been removed +and a single color in any format described in the :ref:`colors_def` tutorial +may be passed. + +.. plot:: + :include-source: true + :alt: Two-panel example contour plots. The left panel has all transparent red contours. The right panel has all dark blue contours. + + import matplotlib.pyplot as plt + + fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(6, 3)) + z = [[0, 1], [1, 2]] + + ax1.contour(z, colors=('r', 0.4)) + ax2.contour(z, colors=(0.1, 0.2, 0.5)) + + plt.show() + +Exception handling control +-------------------------- + +The exception raised when an invalid keyword parameter is passed now includes +that parameter name as the exception's ``name`` property. This provides more +control for exception handling: + + +.. code-block:: python + + import matplotlib.pyplot as plt + + def wobbly_plot(args, **kwargs): + w = kwargs.pop('wobble_factor', None) + + try: + plt.plot(args, **kwargs) + except AttributeError as e: + raise AttributeError(f'wobbly_plot does not take parameter {e.name}') from e + + + wobbly_plot([0, 1], wibble_factor=5) + +.. code-block:: + + AttributeError: wobbly_plot does not take parameter wibble_factor + +Preliminary support for free-threaded CPython 3.13 +-------------------------------------------------- + +Matplotlib 3.10 has preliminary support for the free-threaded build of CPython 3.13. See +https://py-free-threading.github.io, `PEP 703 `_ and +the `CPython 3.13 release notes +`_ for more detail +about free-threaded Python. + +Support for free-threaded Python does not mean that Matplotlib is wholly thread safe. We +expect that use of a Figure within a single thread will work, and though input data is +usually copied, modification of data objects used for a plot from another thread may +cause inconsistencies in cases where it is not. Use of any global state (such as the +``pyplot`` module) is highly discouraged and unlikely to work consistently. Also note +that most GUI toolkits expect to run on the main thread, so interactive usage may be +limited or unsupported from other threads. + +If you are interested in free-threaded Python, for example because you have a +multiprocessing-based workflow that you are interested in running with Python threads, we +encourage testing and experimentation. If you run into problems that you suspect are +because of Matplotlib, please open an issue, checking first if the bug also occurs in the +“regular” non-free-threaded CPython 3.13 build. + +Increased Figure limits with Agg renderer +----------------------------------------- + +Figures using the Agg renderer are now limited to 2**23 pixels in each +direction, instead of 2**16. Additionally, bugs that caused artists to not +render past 2**15 pixels horizontally have been fixed. + +Note that if you are using a GUI backend, it may have its own smaller limits +(which may themselves depend on screen size.) + +Vectorized ``hist`` style parameters +------------------------------------ + +The parameters *hatch*, *edgecolor*, *facecolor*, *linewidth* and *linestyle* +of the `~matplotlib.axes.Axes.hist` method are now vectorized. +This means that you can pass in individual parameters for each histogram +when the input *x* has multiple datasets. + + +.. plot:: + :include-source: true + :alt: Four charts, each displaying stacked histograms of three Poisson distributions. Each chart differentiates the histograms using various parameters: top left uses different linewidths, top right uses different hatches, bottom left uses different edgecolors, and bottom right uses different facecolors. Each histogram on the left side also has a different edgecolor. + + import matplotlib.pyplot as plt + import numpy as np + np.random.seed(19680801) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(9, 9)) + + data1 = np.random.poisson(5, 1000) + data2 = np.random.poisson(7, 1000) + data3 = np.random.poisson(10, 1000) + + labels = ["Data 1", "Data 2", "Data 3"] + + ax1.hist([data1, data2, data3], bins=range(17), histtype="step", stacked=True, + edgecolor=["red", "green", "blue"], linewidth=[1, 2, 3]) + ax1.set_title("Different linewidths") + ax1.legend(labels) + + ax2.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + hatch=["/", ".", "*"]) + ax2.set_title("Different hatch patterns") + ax2.legend(labels) + + ax3.hist([data1, data2, data3], bins=range(17), histtype="bar", fill=False, + edgecolor=["red", "green", "blue"], linestyle=["--", "-.", ":"]) + ax3.set_title("Different linestyles") + ax3.legend(labels) + + ax4.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + facecolor=["red", "green", "blue"]) + ax4.set_title("Different facecolors") + ax4.legend(labels) + + plt.show() + +``InsetIndicator`` artist +------------------------- + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator` which contains the rectangle and +connector patches. These patches now update automatically so that + +.. code-block:: python + + ax.indicate_inset_zoom(ax_inset) + ax_inset.set_xlim(new_lim) + +now gives the same result as + +.. code-block:: python + + ax_inset.set_xlim(new_lim) + ax.indicate_inset_zoom(ax_inset) + +``matplotlib.ticker.EngFormatter`` can computes offsets now +----------------------------------------------------------- + +`matplotlib.ticker.EngFormatter` has gained the ability to show an offset text near the +axis. Using logic shared with `matplotlib.ticker.ScalarFormatter`, it is capable of +deciding whether the data qualifies having an offset and show it with an appropriate SI +quantity prefix, and with the supplied ``unit``. + +To enable this new behavior, simply pass ``useOffset=True`` when you +instantiate `matplotlib.ticker.EngFormatter`. See example +:doc:`/gallery/ticks/engformatter_offset`. + +.. plot:: gallery/ticks/engformatter_offset.py + + +Fix padding of single colorbar for ``ImageGrid`` +------------------------------------------------ + +``ImageGrid`` with ``cbar_mode="single"`` no longer adds the ``axes_pad`` between the +axes and the colorbar for ``cbar_location`` "left" and "bottom". If desired, add additional spacing +using ``cbar_pad``. + +``ax.table`` will accept a pandas DataFrame +-------------------------------------------- + +The `~.axes.Axes.table` method can now accept a Pandas DataFrame for the ``cellText`` argument. + +.. code-block:: python + + import matplotlib.pyplot as plt + import pandas as pd + + data = { + 'Letter': ['A', 'B', 'C'], + 'Number': [100, 200, 300] + } + + df = pd.DataFrame(data) + fig, ax = plt.subplots() + table = ax.table(df, loc='center') # or table = ax.table(cellText=df, loc='center') + ax.axis('off') + plt.show() + + +Subfigures are now added in row-major order +------------------------------------------- + +``Figure.subfigures`` are now added in row-major order for API consistency. + + +.. plot:: + :include-source: true + :alt: Example of creating 3 by 3 subfigures. + + import matplotlib.pyplot as plt + + fig = plt.figure() + subfigs = fig.subfigures(3, 3) + x = np.linspace(0, 10, 100) + + for i, sf in enumerate(fig.subfigs): + ax = sf.subplots() + ax.plot(x, np.sin(x + i), label=f'Subfigure {i+1}') + sf.suptitle(f'Subfigure {i+1}') + ax.set_xticks([]) + ax.set_yticks([]) + plt.show() + +``svg.id`` rcParam +------------------ + +:rc:`svg.id` lets you insert an ``id`` attribute into the top-level ```` tag. + +e.g. ``rcParams["svg.id"] = "svg1"`` results in +default), no ``id`` tag is included + +.. code-block:: XML + + + +This is useful if you would like to link the entire matplotlib SVG file within +another SVG file with the ```` tag. + +.. code-block:: XML + + + + +Where the ``#svg1`` indicator will now refer to the top level ```` tag, and +will hence result in the inclusion of the entire file. + +``boxplot`` and ``bxp`` orientation parameter +--------------------------------------------- + +Boxplots have a new parameter *orientation: {"vertical", "horizontal"}* +to change the orientation of the plot. This replaces the deprecated +*vert: bool* parameter. + + +.. plot:: + :include-source: true + :alt: Example of creating 4 horizontal boxplots. + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots() + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + ax.boxplot(all_data, orientation='horizontal') + plt.show() + + +``violinplot`` and ``violin`` orientation parameter +--------------------------------------------------- + +Violinplots have a new parameter *orientation: {"vertical", "horizontal"}* +to change the orientation of the plot. This will replace the deprecated +*vert: bool* parameter. + + +.. plot:: + :include-source: true + :alt: Example of creating 4 horizontal violinplots. + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots() + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + ax.violinplot(all_data, orientation='horizontal') + plt.show() + +``FillBetweenPolyCollection`` +----------------------------- + +The new class :class:`matplotlib.collections.FillBetweenPolyCollection` provides +the ``set_data`` method, enabling e.g. resampling +(:file:`galleries/event_handling/resample.html`). +:func:`matplotlib.axes.Axes.fill_between` and +:func:`matplotlib.axes.Axes.fill_betweenx` now return this new class. + +.. code-block:: python + + import numpy as np + from matplotlib import pyplot as plt + + t = np.linspace(0, 1) + + fig, ax = plt.subplots() + coll = ax.fill_between(t, -t**2, t**2) + fig.savefig("before.png") + + coll.set_data(t, -t**4, t**4) + fig.savefig("after.png") + +Fill between 3D lines +--------------------- + +The new method `.Axes3D.fill_between` allows to fill the surface between two +3D lines with polygons. + +.. plot:: + :include-source: + :alt: Example of 3D fill_between + + N = 50 + theta = np.linspace(0, 2*np.pi, N) + + x1 = np.cos(theta) + y1 = np.sin(theta) + z1 = 0.1 * np.sin(6 * theta) + + x2 = 0.6 * np.cos(theta) + y2 = 0.6 * np.sin(theta) + z2 = 2 # Note that scalar values work in addition to length N arrays + + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.fill_between(x1, y1, z1, x2, y2, z2, + alpha=0.5, edgecolor='k') + +Rotating 3d plots with the mouse +-------------------------------- + +Rotating three-dimensional plots with the mouse has been made more intuitive. +The plot now reacts the same way to mouse movement, independent of the +particular orientation at hand; and it is possible to control all 3 rotational +degrees of freedom (azimuth, elevation, and roll). By default, +it uses a variation on Ken Shoemake's ARCBALL [1]_. +The particular style of mouse rotation can be set via +:rc:`axes3d.mouserotationstyle`. +See also :ref:`toolkit_mouse-rotation`. + +To revert to the original mouse rotation style, +create a file ``matplotlibrc`` with contents:: + + axes3d.mouserotationstyle: azel + +To try out one of the various mouse rotation styles: + +.. code:: + + import matplotlib as mpl + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball' + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib import cm + + ax = plt.figure().add_subplot(projection='3d') + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + + surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm, + linewidth=0, antialiased=False) + + plt.show() + + +.. [1] Ken Shoemake, "ARCBALL: A user interface for specifying + three-dimensional rotation using a mouse", in Proceedings of Graphics + Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 + + + +Data in 3D plots can now be dynamically clipped to the axes view limits +----------------------------------------------------------------------- + +All 3D plotting functions now support the *axlim_clip* keyword argument, which +will clip the data to the axes view limits, hiding all data outside those +bounds. This clipping will be dynamically applied in real time while panning +and zooming. + +Please note that if one vertex of a line segment or 3D patch is clipped, then +the entire segment or patch will be hidden. Not being able to show partial +lines or patches such that they are "smoothly" cut off at the boundaries of the +view box is a limitation of the current renderer. + +.. plot:: + :include-source: true + :alt: Example of default behavior (blue) and axlim_clip=True (orange) + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + x = np.arange(-5, 5, 0.5) + y = np.arange(-5, 5, 0.5) + X, Y = np.meshgrid(x, y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + + # Note that when a line has one vertex outside the view limits, the entire + # line is hidden. The same is true for 3D patches (not shown). + # In this example, data where x < 0 or z > 0.5 is clipped. + ax.plot_wireframe(X, Y, Z, color='C0') + ax.plot_wireframe(X, Y, Z, color='C1', axlim_clip=True) + ax.set(xlim=(0, 10), ylim=(-5, 5), zlim=(-1, 0.5)) + ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) + + +Miscellaneous Changes +--------------------- + +- The `matplotlib.ticker.ScalarFormatter` class has gained a new instantiating parameter ``usetex``. +- Creating an Axes is now 20-25% faster due to internal optimizations. +- The API on `.Figure.subfigures` and `.SubFigure` are now considered stable. diff --git a/doc/users/prev_whats_new/whats_new_3.3.0.rst b/doc/users/prev_whats_new/whats_new_3.3.0.rst index 00ea10620d14..94914bcc75db 100644 --- a/doc/users/prev_whats_new/whats_new_3.3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.3.0.rst @@ -292,8 +292,8 @@ positioning. For the xlabel, the supported values are 'left', 'center', or 'right'. For the ylabel, the supported values are 'bottom', 'center', or 'top'. -The default is controlled via :rc:`xaxis.labelposition` and -:rc:`yaxis.labelposition`; the Colorbar label takes the rcParam based on its +The default is controlled via :rc:`xaxis.labellocation` and +:rc:`yaxis.labellocation`; the Colorbar label takes the rcParam based on its orientation. .. plot:: diff --git a/doc/users/prev_whats_new/whats_new_3.6.0.rst b/doc/users/prev_whats_new/whats_new_3.6.0.rst index 859bbb47e354..9fcf8cebfc6f 100644 --- a/doc/users/prev_whats_new/whats_new_3.6.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.6.0.rst @@ -217,7 +217,7 @@ Linestyles for negative contours may be set individually The line style of negative contours may be set by passing the *negative_linestyles* argument to `.Axes.contour`. Previously, this style could -only be set globally via :rc:`contour.negative_linestyles`. +only be set globally via :rc:`contour.negative_linestyle`. .. plot:: :alt: Two contour plots, each showing two positive and two negative contours. The positive contours are shown in solid black lines in both plots. In one plot the negative contours are shown in dashed lines, which is the current styling. In the other plot they're shown in dotted lines, which is one of the new options. diff --git a/doc/users/prev_whats_new/whats_new_3.8.0.rst b/doc/users/prev_whats_new/whats_new_3.8.0.rst index 8c34252098db..88f987172adb 100644 --- a/doc/users/prev_whats_new/whats_new_3.8.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.8.0.rst @@ -496,7 +496,7 @@ Other improvements macosx: New figures can be opened in either windows or tabs ----------------------------------------------------------- -There is a new :rc:`macosx.window_mode`` rcParam to control how +There is a new :rc:`macosx.window_mode` rcParam to control how new figures are opened with the macosx backend. The default is **system** which uses the system settings, or one can specify either **tab** or **window** to explicitly choose the mode used to open new figures. diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index 010f9b7534bc..3bb30bf2fa49 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -12,6 +12,14 @@ Release notes .. include:: release_notes_next.rst +Version 3.10 +^^^^^^^^^^^^ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.10.0.rst + ../api/prev_api_changes/api_changes_3.10.0.rst + prev_whats_new/github_stats_3.10.0.rst Version 3.9 ^^^^^^^^^^^ @@ -22,7 +30,8 @@ Version 3.9 ../api/prev_api_changes/api_changes_3.9.2.rst ../api/prev_api_changes/api_changes_3.9.1.rst ../api/prev_api_changes/api_changes_3.9.0.rst - github_stats.rst + prev_whats_new/github_stats_3.9.4.rst + prev_whats_new/github_stats_3.9.3.rst prev_whats_new/github_stats_3.9.2.rst prev_whats_new/github_stats_3.9.1.rst prev_whats_new/github_stats_3.9.0.rst diff --git a/doc/users/resources/index.rst b/doc/users/resources/index.rst index 77010f176048..7e2339ee8191 100644 --- a/doc/users/resources/index.rst +++ b/doc/users/resources/index.rst @@ -54,8 +54,8 @@ Videos `_ by Eric Jones * `Anatomy of Matplotlib - `_ - by Benjamin Root + `_ + by Benjamin Root and Hannah Aizenman * `Data Visualization Basics with Python (O'Reilly) `_ diff --git a/environment.yml b/environment.yml index 2930ccf17e83..4a36f76031ea 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ # # conda env create -f environment.yml # conda activate mpl-dev -# pip install -e .[dev] +# pip install --verbose --no-build-isolation --editable ".[dev]" # --- name: mpl-dev @@ -16,14 +16,15 @@ dependencies: - fonttools>=4.22.0 - importlib-resources>=3.2.0 - kiwisolver>=1.3.1 - - pybind11>=2.6.0 + - pybind11>=2.13.2 - meson-python>=0.13.1 - - numpy>=1.23 + - numpy<2.1 - pillow>=8 - pkg-config - pygobject - pyparsing>=2.3.1 - pyqt + - python>=3.10 - python-dateutil>=2.1 - setuptools_scm - wxpython @@ -39,12 +40,15 @@ dependencies: - sphinx>=3.0.0,!=6.1.2 - sphinx-copybutton - sphinx-gallery>=0.12.0 + - joblib # needed for sphinx-gallery[parallel] - sphinx-design - - sphinx-tags>=0.3.0 + - sphinx-tags>=0.4.0 + - pystemmer - pip - pip: - mpl-sphinx-theme~=3.8.0 - sphinxcontrib-svg2pdfconverter>=1.1.0 + - sphinxcontrib-video>=0.2.1 - pikepdf # testing - black<24 diff --git a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h index d1cc705405dc..44a55417b492 100644 --- a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h +++ b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h @@ -325,8 +325,10 @@ namespace agg if(dx >= dx_limit || dx <= -dx_limit) { - int cx = (x1 + x2) >> 1; - int cy = (y1 + y2) >> 1; + // These are overflow safe versions of (x1 + x2) >> 1; divide each by 2 + // first, then add 1 if both were odd. + int cx = (x1 >> 1) + (x2 >> 1) + ((x1 & 1) & (x2 & 1)); + int cy = (y1 >> 1) + (y2 >> 1) + ((y1 & 1) & (y2 & 1)); line(x1, y1, cx, cy); line(cx, cy, x2, y2); return; diff --git a/extern/agg24-svn/include/agg_span_image_filter_gray.h b/extern/agg24-svn/include/agg_span_image_filter_gray.h index e2c688e004cb..7ca583af724d 100644 --- a/extern/agg24-svn/include/agg_span_image_filter_gray.h +++ b/extern/agg24-svn/include/agg_span_image_filter_gray.h @@ -397,7 +397,9 @@ namespace agg fg += weight * *fg_ptr; fg >>= image_filter_shift; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -491,8 +493,10 @@ namespace agg } fg = color_type::downshift(fg, image_filter_shift); +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -593,8 +597,10 @@ namespace agg } fg /= total_weight; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -701,8 +707,10 @@ namespace agg } fg /= total_weight; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); diff --git a/extern/meson.build b/extern/meson.build index 662feb7872da..5463183a9099 100644 --- a/extern/meson.build +++ b/extern/meson.build @@ -1,6 +1,5 @@ # Bundled code. subdir('agg24-svn') -subdir('ttconv') # External code. diff --git a/extern/ttconv/meson.build b/extern/ttconv/meson.build deleted file mode 100644 index 939eb3069c43..000000000000 --- a/extern/ttconv/meson.build +++ /dev/null @@ -1,14 +0,0 @@ -ttconv_lib = static_library('ttconv', - 'pprdrv_tt2.cpp', - 'pprdrv_tt.cpp', - 'ttutil.cpp', - 'pprdrv.h', - 'truetype.h', - dependencies: [py3_dep], - gnu_symbol_visibility: 'inlineshidden', -) - -ttconv_dep = declare_dependency( - include_directories: include_directories('.'), - link_with: ttconv_lib, -) diff --git a/extern/ttconv/pprdrv.h b/extern/ttconv/pprdrv.h deleted file mode 100644 index 8c0b6c195564..000000000000 --- a/extern/ttconv/pprdrv.h +++ /dev/null @@ -1,102 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* - * Modified for use within matplotlib - * 5 July 2007 - * Michael Droettboom - */ - -/* -** ~ppr/src/include/pprdrv.h -** Copyright 1995, Trinity College Computing Center. -** Written by David Chappell. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -** -** This file last revised 5 December 1995. -*/ - -#include -#include - -/* - * Encapsulates all of the output to write to an arbitrary output - * function. This both removes the hardcoding of output to go to stdout - * and makes output thread-safe. Michael Droettboom [06-07-07] - */ -class TTStreamWriter -{ - private: - // Private copy and assignment - TTStreamWriter& operator=(const TTStreamWriter& other); - TTStreamWriter(const TTStreamWriter& other); - - public: - TTStreamWriter() { } - virtual ~TTStreamWriter() { } - - virtual void write(const char*) = 0; - - virtual void printf(const char* format, ...); - virtual void put_char(int val); - virtual void puts(const char* a); - virtual void putline(const char* a); -}; - -void replace_newlines_with_spaces(char* a); - -/* - * A simple class for all ttconv exceptions. - */ -class TTException -{ - const char* message; - TTException& operator=(const TTStreamWriter& other); - TTException(const TTStreamWriter& other); - -public: - TTException(const char* message_) : message(message_) { } - const char* getMessage() - { - return message; - } -}; - -/* -** No debug code will be included if this -** is not defined: -*/ -/* #define DEBUG 1 */ - -/* -** Uncomment the defines for the debugging -** code you want to have included. -*/ -#ifdef DEBUG -#define DEBUG_TRUETYPE /* truetype fonts, conversion to Postscript */ -#endif - -#if DEBUG_TRUETYPE -#define debug(...) printf(__VA_ARGS__) -#else -#define debug(...) -#endif - -/* Do not change anything below this line. */ - -enum font_type_enum -{ - PS_TYPE_3 = 3, - PS_TYPE_42 = 42, - PS_TYPE_42_3_HYBRID = 43, -}; - -/* routines in pprdrv_tt.c */ -void insert_ttfont(const char *filename, TTStreamWriter& stream, font_type_enum target_type, std::vector& glyph_ids); - -/* end of file */ diff --git a/extern/ttconv/pprdrv_tt.cpp b/extern/ttconv/pprdrv_tt.cpp deleted file mode 100644 index a0c724c8aa11..000000000000 --- a/extern/ttconv/pprdrv_tt.cpp +++ /dev/null @@ -1,1401 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* - * Modified for use within matplotlib - * 5 July 2007 - * Michael Droettboom - */ - -/* -** ~ppr/src/pprdrv/pprdrv_tt.c -** Copyright 1995, Trinity College Computing Center. -** Written by David Chappell. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -** -** TrueType font support. These functions allow PPR to generate -** PostScript fonts from Microsoft compatible TrueType font files. -** -** Last revised 19 December 1995. -*/ - -#include -#include -#include -#include "pprdrv.h" -#include "truetype.h" -#include -#ifdef _POSIX_C_SOURCE -# undef _POSIX_C_SOURCE -#endif -#ifndef _AIX -#ifdef _XOPEN_SOURCE -# undef _XOPEN_SOURCE -#endif -#endif -#include - -/*========================================================================== -** Convert the indicated Truetype font file to a type 42 or type 3 -** PostScript font and insert it in the output stream. -** -** All the routines from here to the end of file file are involved -** in this process. -==========================================================================*/ - -/*--------------------------------------- -** Endian conversion routines. -** These routines take a BYTE pointer -** and return a value formed by reading -** bytes starting at that point. -** -** These routines read the big-endian -** values which are used in TrueType -** font files. ----------------------------------------*/ - -/* -** Get an Unsigned 32 bit number. -*/ -ULONG getULONG(BYTE *p) -{ - int x; - ULONG val=0; - - for (x=0; x<4; x++) - { - val *= 0x100; - val += p[x]; - } - - return val; -} /* end of ftohULONG() */ - -/* -** Get an unsigned 16 bit number. -*/ -USHORT getUSHORT(BYTE *p) -{ - int x; - USHORT val=0; - - for (x=0; x<2; x++) - { - val *= 0x100; - val += p[x]; - } - - return val; -} /* end of getUSHORT() */ - -/* -** Get a 32 bit fixed point (16.16) number. -** A special structure is used to return the value. -*/ -Fixed getFixed(BYTE *s) -{ - Fixed val={0,0}; - - val.whole = ((s[0] * 256) + s[1]); - val.fraction = ((s[2] * 256) + s[3]); - - return val; -} /* end of getFixed() */ - -/*----------------------------------------------------------------------- -** Load a TrueType font table into memory and return a pointer to it. -** The font's "file" and "offset_table" fields must be set before this -** routine is called. -** -** This first argument is a TrueType font structure, the second -** argument is the name of the table to retrieve. A table name -** is always 4 characters, though the last characters may be -** padding spaces. ------------------------------------------------------------------------*/ -BYTE *GetTable(struct TTFONT *font, const char *name) -{ - BYTE *ptr; - ULONG x; - debug("GetTable(file,font,\"%s\")",name); - - /* We must search the table directory. */ - ptr = font->offset_table + 12; - x=0; - while (true) - { - if ( strncmp((const char*)ptr,name,4) == 0 ) - { - ULONG offset,length; - BYTE *table; - - offset = getULONG( ptr + 8 ); - length = getULONG( ptr + 12 ); - table = (BYTE*)calloc( sizeof(BYTE), length + 2 ); - - try - { - debug("Loading table \"%s\" from offset %d, %d bytes",name,offset,length); - - if ( fseek( font->file, (long)offset, SEEK_SET ) ) - { - throw TTException("TrueType font may be corrupt (reason 3)"); - } - - if ( fread(table,sizeof(BYTE),length,font->file) != (sizeof(BYTE) * length)) - { - throw TTException("TrueType font may be corrupt (reason 4)"); - } - } - catch (TTException& ) - { - free(table); - throw; - } - /* Always NUL-terminate; add two in case of UTF16 strings. */ - table[length] = '\0'; - table[length + 1] = '\0'; - return table; - } - - x++; - ptr += 16; - if (x == font->numTables) - { - throw TTException("TrueType font is missing table"); - } - } - -} /* end of GetTable() */ - -static void utf16be_to_ascii(char *dst, char *src, size_t length) { - ++src; - for (; *src != 0 && length; dst++, src += 2, --length) { - *dst = *src; - } -} - -/*-------------------------------------------------------------------- -** Load the 'name' table, get information from it, -** and store that information in the font structure. -** -** The 'name' table contains information such as the name of -** the font, and it's PostScript name. ---------------------------------------------------------------------*/ -void Read_name(struct TTFONT *font) -{ - BYTE *table_ptr,*ptr2; - int numrecords; /* Number of strings in this table */ - BYTE *strings; /* pointer to start of string storage */ - int x; - int platform; /* Current platform id */ - int nameid; /* name id, */ - int offset,length; /* offset and length of string. */ - debug("Read_name()"); - - table_ptr = NULL; - - /* Set default values to avoid future references to undefined - * pointers. Allocate each of PostName, FullName, FamilyName, - * Version, and Style separately so they can be freed safely. */ - for (char **ptr = &(font->PostName); ptr != NULL; ) - { - *ptr = (char*) calloc(sizeof(char), strlen("unknown")+1); - strcpy(*ptr, "unknown"); - if (ptr == &(font->PostName)) ptr = &(font->FullName); - else if (ptr == &(font->FullName)) ptr = &(font->FamilyName); - else if (ptr == &(font->FamilyName)) ptr = &(font->Version); - else if (ptr == &(font->Version)) ptr = &(font->Style); - else ptr = NULL; - } - font->Copyright = font->Trademark = (char*)NULL; - - table_ptr = GetTable(font, "name"); /* pointer to table */ - try - { - numrecords = getUSHORT( table_ptr + 2 ); /* number of names */ - strings = table_ptr + getUSHORT( table_ptr + 4 ); /* start of string storage */ - - ptr2 = table_ptr + 6; - for (x=0; x < numrecords; x++,ptr2+=12) - { - platform = getUSHORT(ptr2); - nameid = getUSHORT(ptr2+6); - length = getUSHORT(ptr2+8); - offset = getUSHORT(ptr2+10); - debug("platform %d, encoding %d, language 0x%x, name %d, offset %d, length %d", - platform,encoding,language,nameid,offset,length); - - /* Copyright notice */ - if ( platform == 1 && nameid == 0 ) - { - font->Copyright = (char*)calloc(sizeof(char),length+1); - strncpy(font->Copyright,(const char*)strings+offset,length); - font->Copyright[length]='\0'; - replace_newlines_with_spaces(font->Copyright); - debug("font->Copyright=\"%s\"",font->Copyright); - continue; - } - - - /* Font Family name */ - if ( platform == 1 && nameid == 1 ) - { - free(font->FamilyName); - font->FamilyName = (char*)calloc(sizeof(char),length+1); - strncpy(font->FamilyName,(const char*)strings+offset,length); - font->FamilyName[length]='\0'; - replace_newlines_with_spaces(font->FamilyName); - debug("font->FamilyName=\"%s\"",font->FamilyName); - continue; - } - - - /* Font Family name */ - if ( platform == 1 && nameid == 2 ) - { - free(font->Style); - font->Style = (char*)calloc(sizeof(char),length+1); - strncpy(font->Style,(const char*)strings+offset,length); - font->Style[length]='\0'; - replace_newlines_with_spaces(font->Style); - debug("font->Style=\"%s\"",font->Style); - continue; - } - - - /* Full Font name */ - if ( platform == 1 && nameid == 4 ) - { - free(font->FullName); - font->FullName = (char*)calloc(sizeof(char),length+1); - strncpy(font->FullName,(const char*)strings+offset,length); - font->FullName[length]='\0'; - replace_newlines_with_spaces(font->FullName); - debug("font->FullName=\"%s\"",font->FullName); - continue; - } - - - /* Version string */ - if ( platform == 1 && nameid == 5 ) - { - free(font->Version); - font->Version = (char*)calloc(sizeof(char),length+1); - strncpy(font->Version,(const char*)strings+offset,length); - font->Version[length]='\0'; - replace_newlines_with_spaces(font->Version); - debug("font->Version=\"%s\"",font->Version); - continue; - } - - - /* PostScript name */ - if ( platform == 1 && nameid == 6 ) - { - free(font->PostName); - font->PostName = (char*)calloc(sizeof(char),length+1); - strncpy(font->PostName,(const char*)strings+offset,length); - font->PostName[length]='\0'; - replace_newlines_with_spaces(font->PostName); - debug("font->PostName=\"%s\"",font->PostName); - continue; - } - - /* Microsoft-format PostScript name */ - if ( platform == 3 && nameid == 6 ) - { - free(font->PostName); - font->PostName = (char*)calloc(sizeof(char),length+1); - utf16be_to_ascii(font->PostName, (char *)strings+offset, length); - font->PostName[length/2]='\0'; - replace_newlines_with_spaces(font->PostName); - debug("font->PostName=\"%s\"",font->PostName); - continue; - } - - - /* Trademark string */ - if ( platform == 1 && nameid == 7 ) - { - font->Trademark = (char*)calloc(sizeof(char),length+1); - strncpy(font->Trademark,(const char*)strings+offset,length); - font->Trademark[length]='\0'; - replace_newlines_with_spaces(font->Trademark); - debug("font->Trademark=\"%s\"",font->Trademark); - continue; - } - } - } - catch (TTException& ) - { - free(table_ptr); - throw; - } - - free(table_ptr); -} /* end of Read_name() */ - -/*--------------------------------------------------------------------- -** Write the header for a PostScript font. ----------------------------------------------------------------------*/ -void ttfont_header(TTStreamWriter& stream, struct TTFONT *font) -{ - int VMMin; - int VMMax; - - /* - ** To show that it is a TrueType font in PostScript format, - ** we will begin the file with a specific string. - ** This string also indicates the version of the TrueType - ** specification on which the font is based and the - ** font manufacturer's revision number for the font. - */ - if ( font->target_type == PS_TYPE_42 || - font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.printf("%%!PS-TrueTypeFont-%d.%d-%d.%d\n", - font->TTVersion.whole, font->TTVersion.fraction, - font->MfrRevision.whole, font->MfrRevision.fraction); - } - - /* If it is not a Type 42 font, we will use a different format. */ - else - { - stream.putline("%!PS-Adobe-3.0 Resource-Font"); - } /* See RBIIp 641 */ - - /* We will make the title the name of the font. */ - stream.printf("%%%%Title: %s\n",font->FullName); - - /* If there is a Copyright notice, put it here too. */ - if ( font->Copyright != (char*)NULL ) - { - stream.printf("%%%%Copyright: %s\n",font->Copyright); - } - - /* We created this file. */ - if ( font->target_type == PS_TYPE_42 ) - { - stream.putline("%%Creator: Converted from TrueType to type 42 by PPR"); - } - else if (font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.putline("%%Creator: Converted from TypeType to type 42/type 3 hybrid by PPR"); - } - else - { - stream.putline("%%Creator: Converted from TrueType to type 3 by PPR"); - } - - /* If VM usage information is available, print it. */ - if ( font->target_type == PS_TYPE_42 || font->target_type == PS_TYPE_42_3_HYBRID) - { - VMMin = (int)getULONG( font->post_table + 16 ); - VMMax = (int)getULONG( font->post_table + 20 ); - if ( VMMin > 0 && VMMax > 0 ) - stream.printf("%%%%VMUsage: %d %d\n",VMMin,VMMax); - } - - /* Start the dictionary which will eventually */ - /* become the font. */ - if (font->target_type == PS_TYPE_42) - { - stream.putline("15 dict begin"); - } - else - { - stream.putline("25 dict begin"); - - /* Type 3 fonts will need some subroutines here. */ - stream.putline("/_d{bind def}bind def"); - stream.putline("/_m{moveto}_d"); - stream.putline("/_l{lineto}_d"); - stream.putline("/_cl{closepath eofill}_d"); - stream.putline("/_c{curveto}_d"); - stream.putline("/_sc{7 -1 roll{setcachedevice}{pop pop pop pop pop pop}ifelse}_d"); - stream.putline("/_e{exec}_d"); - } - - stream.printf("/FontName /%s def\n",font->PostName); - stream.putline("/PaintType 0 def"); - - if (font->target_type == PS_TYPE_42 || font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.putline("/FontMatrix[1 0 0 1 0 0]def"); - } - else - { - stream.putline("/FontMatrix[.001 0 0 .001 0 0]def"); - } - - stream.printf("/FontBBox[%d %d %d %d]def\n",font->llx-1,font->lly-1,font->urx,font->ury); - if (font->target_type == PS_TYPE_42 || font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.printf("/FontType 42 def\n", font->target_type ); - } - else - { - stream.printf("/FontType 3 def\n", font->target_type ); - } -} /* end of ttfont_header() */ - -/*------------------------------------------------------------- -** Define the encoding array for this font. -** Since we don't really want to deal with converting all of -** the possible font encodings in the wild to a standard PS -** one, we just explicitly create one for each font. --------------------------------------------------------------*/ -void ttfont_encoding(TTStreamWriter& stream, struct TTFONT *font, std::vector& glyph_ids, font_type_enum target_type) -{ - if (target_type == PS_TYPE_3 || target_type == PS_TYPE_42_3_HYBRID) - { - stream.printf("/Encoding [ "); - - for (std::vector::const_iterator i = glyph_ids.begin(); - i != glyph_ids.end(); ++i) - { - const char* name = ttfont_CharStrings_getname(font, *i); - stream.printf("/%s ", name); - } - - stream.printf("] def\n"); - } - else - { - stream.putline("/Encoding StandardEncoding def"); - } -} /* end of ttfont_encoding() */ - -/*----------------------------------------------------------- -** Create the optional "FontInfo" sub-dictionary. ------------------------------------------------------------*/ -void ttfont_FontInfo(TTStreamWriter& stream, struct TTFONT *font) -{ - Fixed ItalicAngle; - - /* We create a sub dictionary named "FontInfo" where we */ - /* store information which though it is not used by the */ - /* interpreter, is useful to some programs which will */ - /* be printing with the font. */ - stream.putline("/FontInfo 10 dict dup begin"); - - /* These names come from the TrueType font's "name" table. */ - stream.printf("/FamilyName (%s) def\n",font->FamilyName); - stream.printf("/FullName (%s) def\n",font->FullName); - - if ( font->Copyright != (char*)NULL || font->Trademark != (char*)NULL ) - { - stream.printf("/Notice (%s", - font->Copyright != (char*)NULL ? font->Copyright : ""); - stream.printf("%s%s) def\n", - font->Trademark != (char*)NULL ? " " : "", - font->Trademark != (char*)NULL ? font->Trademark : ""); - } - - /* This information is not quite correct. */ - stream.printf("/Weight (%s) def\n",font->Style); - - /* Some fonts have this as "version". */ - stream.printf("/Version (%s) def\n",font->Version); - - /* Some information from the "post" table. */ - ItalicAngle = getFixed( font->post_table + 4 ); - stream.printf("/ItalicAngle %d.%d def\n",ItalicAngle.whole,ItalicAngle.fraction); - stream.printf("/isFixedPitch %s def\n", getULONG( font->post_table + 12 ) ? "true" : "false" ); - stream.printf("/UnderlinePosition %d def\n", (int)getFWord( font->post_table + 8 ) ); - stream.printf("/UnderlineThickness %d def\n", (int)getFWord( font->post_table + 10 ) ); - stream.putline("end readonly def"); -} /* end of ttfont_FontInfo() */ - -/*------------------------------------------------------------------- -** sfnts routines -** These routines generate the PostScript "sfnts" array which -** contains one or more strings which contain a reduced version -** of the TrueType font. -** -** A number of functions are required to accomplish this rather -** complicated task. --------------------------------------------------------------------*/ -int string_len; -int line_len; -bool in_string; - -/* -** This is called once at the start. -*/ -void sfnts_start(TTStreamWriter& stream) -{ - stream.puts("/sfnts[<"); - in_string=true; - string_len=0; - line_len=8; -} /* end of sfnts_start() */ - -/* -** Write a BYTE as a hexadecimal value as part of the sfnts array. -*/ -void sfnts_pputBYTE(TTStreamWriter& stream, BYTE n) -{ - static const char hexdigits[]="0123456789ABCDEF"; - - if (!in_string) - { - stream.put_char('<'); - string_len=0; - line_len++; - in_string=true; - } - - stream.put_char( hexdigits[ n / 16 ] ); - stream.put_char( hexdigits[ n % 16 ] ); - string_len++; - line_len+=2; - - if (line_len > 70) - { - stream.put_char('\n'); - line_len=0; - } - -} /* end of sfnts_pputBYTE() */ - -/* -** Write a USHORT as a hexadecimal value as part of the sfnts array. -*/ -void sfnts_pputUSHORT(TTStreamWriter& stream, USHORT n) -{ - sfnts_pputBYTE(stream, n / 256); - sfnts_pputBYTE(stream, n % 256); -} /* end of sfnts_pputUSHORT() */ - -/* -** Write a ULONG as part of the sfnts array. -*/ -void sfnts_pputULONG(TTStreamWriter& stream, ULONG n) -{ - int x1,x2,x3; - - x1 = n % 256; - n /= 256; - x2 = n % 256; - n /= 256; - x3 = n % 256; - n /= 256; - - sfnts_pputBYTE(stream, n); - sfnts_pputBYTE(stream, x3); - sfnts_pputBYTE(stream, x2); - sfnts_pputBYTE(stream, x1); -} /* end of sfnts_pputULONG() */ - -/* -** This is called whenever it is -** necessary to end a string in the sfnts array. -** -** (The array must be broken into strings which are -** no longer than 64K characters.) -*/ -void sfnts_end_string(TTStreamWriter& stream) -{ - if (in_string) - { - string_len=0; /* fool sfnts_pputBYTE() */ - -#ifdef DEBUG_TRUETYPE_INLINE - puts("\n% dummy byte:\n"); -#endif - - sfnts_pputBYTE(stream, 0); /* extra byte for pre-2013 compatibility */ - stream.put_char('>'); - line_len++; - } - in_string=false; -} /* end of sfnts_end_string() */ - -/* -** This is called at the start of each new table. -** The argement is the length in bytes of the table -** which will follow. If the new table will not fit -** in the current string, a new one is started. -*/ -void sfnts_new_table(TTStreamWriter& stream, ULONG length) -{ - if ( (string_len + length) > 65528 ) - sfnts_end_string(stream); -} /* end of sfnts_new_table() */ - -/* -** We may have to break up the 'glyf' table. That is the reason -** why we provide this special routine to copy it into the sfnts -** array. -*/ -void sfnts_glyf_table(TTStreamWriter& stream, struct TTFONT *font, ULONG oldoffset, ULONG correct_total_length) -{ - ULONG off; - ULONG length; - int c; - ULONG total=0; /* running total of bytes written to table */ - int x; - bool loca_is_local=false; - debug("sfnts_glyf_table(font,%d)", (int)correct_total_length); - - if (font->loca_table == NULL) - { - font->loca_table = GetTable(font,"loca"); - loca_is_local = true; - } - - /* Seek to proper position in the file. */ - fseek( font->file, oldoffset, SEEK_SET ); - - /* Copy the glyphs one by one */ - for (x=0; x < font->numGlyphs; x++) - { - /* Read the glyph offset from the index-to-location table. */ - if (font->indexToLocFormat == 0) - { - off = getUSHORT( font->loca_table + (x * 2) ); - off *= 2; - length = getUSHORT( font->loca_table + ((x+1) * 2) ); - length *= 2; - length -= off; - } - else - { - off = getULONG( font->loca_table + (x * 4) ); - length = getULONG( font->loca_table + ((x+1) * 4) ); - length -= off; - } - debug("glyph length=%d",(int)length); - - /* Start new string if necessary. */ - sfnts_new_table( stream, (int)length ); - - /* - ** Make sure the glyph is padded out to a - ** two byte boundary. - */ - if ( length % 2 ) { - throw TTException("TrueType font contains a 'glyf' table without 2 byte padding"); - } - - /* Copy the bytes of the glyph. */ - while ( length-- ) - { - if ( (c = fgetc(font->file)) == EOF ) { - throw TTException("TrueType font may be corrupt (reason 6)"); - } - - sfnts_pputBYTE(stream, c); - total++; /* add to running total */ - } - - } - - if (loca_is_local) - { - free(font->loca_table); - font->loca_table = NULL; - } - - /* Pad out to full length from table directory */ - while ( total < correct_total_length ) - { - sfnts_pputBYTE(stream, 0); - total++; - } - -} /* end of sfnts_glyf_table() */ - -/* -** Here is the routine which ties it all together. -** -** Create the array called "sfnts" which -** holds the actual TrueType data. -*/ -void ttfont_sfnts(TTStreamWriter& stream, struct TTFONT *font) -{ - static const char *table_names[] = /* The names of all tables */ - { - /* which it is worth while */ - "cvt ", /* to include in a Type 42 */ - "fpgm", /* PostScript font. */ - "glyf", - "head", - "hhea", - "hmtx", - "loca", - "maxp", - "prep" - } ; - - struct /* The location of each of */ - { - ULONG oldoffset; /* the above tables. */ - ULONG newoffset; - ULONG length; - ULONG checksum; - } tables[9]; - - BYTE *ptr; /* A pointer into the origional table directory. */ - ULONG x,y; /* General use loop countes. */ - int c; /* Input character. */ - int diff; - ULONG nextoffset; - int count; /* How many `important' tables did we find? */ - - ptr = font->offset_table + 12; - nextoffset=0; - count=0; - - /* - ** Find the tables we want and store there vital - ** statistics in tables[]. - */ - ULONG num_tables_read = 0; /* Number of tables read from the directory */ - for (x = 0; x < 9; x++) { - do { - if (num_tables_read < font->numTables) { - /* There are still tables to read from ptr */ - diff = strncmp((char*)ptr, table_names[x], 4); - - if (diff > 0) { /* If we are past it. */ - tables[x].length = 0; - diff = 0; - } else if (diff < 0) { /* If we haven't hit it yet. */ - ptr += 16; - num_tables_read++; - } else if (diff == 0) { /* Here it is! */ - tables[x].newoffset = nextoffset; - tables[x].checksum = getULONG( ptr + 4 ); - tables[x].oldoffset = getULONG( ptr + 8 ); - tables[x].length = getULONG( ptr + 12 ); - nextoffset += ( ((tables[x].length + 3) / 4) * 4 ); - count++; - ptr += 16; - num_tables_read++; - } - } else { - /* We've read the whole table directory already */ - /* Some tables couldn't be found */ - tables[x].length = 0; - break; /* Proceed to next tables[x] */ - } - } while (diff != 0); - - } /* end of for loop which passes over the table directory */ - - /* Begin the sfnts array. */ - sfnts_start(stream); - - /* Generate the offset table header */ - /* Start by copying the TrueType version number. */ - ptr = font->offset_table; - for (x=0; x < 4; x++) - { - sfnts_pputBYTE( stream, *(ptr++) ); - } - - /* Now, generate those silly numTables numbers. */ - sfnts_pputUSHORT(stream, count); /* number of tables */ - - int search_range = 1; - int entry_sel = 0; - - while (search_range <= count) { - search_range <<= 1; - entry_sel++; - } - entry_sel = entry_sel > 0 ? entry_sel - 1 : 0; - search_range = (search_range >> 1) * 16; - int range_shift = count * 16 - search_range; - - sfnts_pputUSHORT(stream, search_range); /* searchRange */ - sfnts_pputUSHORT(stream, entry_sel); /* entrySelector */ - sfnts_pputUSHORT(stream, range_shift); /* rangeShift */ - - debug("only %d tables selected",count); - - /* Now, emmit the table directory. */ - for (x=0; x < 9; x++) - { - if ( tables[x].length == 0 ) /* Skip missing tables */ - { - continue; - } - - /* Name */ - sfnts_pputBYTE( stream, table_names[x][0] ); - sfnts_pputBYTE( stream, table_names[x][1] ); - sfnts_pputBYTE( stream, table_names[x][2] ); - sfnts_pputBYTE( stream, table_names[x][3] ); - - /* Checksum */ - sfnts_pputULONG( stream, tables[x].checksum ); - - /* Offset */ - sfnts_pputULONG( stream, tables[x].newoffset + 12 + (count * 16) ); - - /* Length */ - sfnts_pputULONG( stream, tables[x].length ); - } - - /* Now, send the tables */ - for (x=0; x < 9; x++) - { - if ( tables[x].length == 0 ) /* skip tables that aren't there */ - { - continue; - } - debug("emmiting table '%s'",table_names[x]); - - /* 'glyf' table gets special treatment */ - if ( strcmp(table_names[x],"glyf")==0 ) - { - sfnts_glyf_table(stream,font,tables[x].oldoffset,tables[x].length); - } - else /* Other tables may not exceed */ - { - /* 65535 bytes in length. */ - if ( tables[x].length > 65535 ) - { - throw TTException("TrueType font has a table which is too long"); - } - - /* Start new string if necessary. */ - sfnts_new_table(stream, tables[x].length); - - /* Seek to proper position in the file. */ - fseek( font->file, tables[x].oldoffset, SEEK_SET ); - - /* Copy the bytes of the table. */ - for ( y=0; y < tables[x].length; y++ ) - { - if ( (c = fgetc(font->file)) == EOF ) - { - throw TTException("TrueType font may be corrupt (reason 7)"); - } - - sfnts_pputBYTE(stream, c); - } - } - - /* Padd it out to a four byte boundary. */ - y=tables[x].length; - while ( (y % 4) != 0 ) - { - sfnts_pputBYTE(stream, 0); - y++; -#ifdef DEBUG_TRUETYPE_INLINE - puts("\n% pad byte:\n"); -#endif - } - - } /* End of loop for all tables */ - - /* Close the array. */ - sfnts_end_string(stream); - stream.putline("]def"); -} /* end of ttfont_sfnts() */ - -/*-------------------------------------------------------------- -** Create the CharStrings dictionary which will translate -** PostScript character names to TrueType font character -** indexes. -** -** If we are creating a type 3 instead of a type 42 font, -** this array will instead convert PostScript character names -** to executable proceedures. ---------------------------------------------------------------*/ -const char *Apple_CharStrings[]= -{ - ".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign", - "dollar","percent","ampersand","quotesingle","parenleft","parenright", - "asterisk","plus", "comma","hyphen","period","slash","zero","one","two", - "three","four","five","six","seven","eight","nine","colon","semicolon", - "less","equal","greater","question","at","A","B","C","D","E","F","G","H","I", - "J","K", "L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z", - "bracketleft","backslash","bracketright","asciicircum","underscore","grave", - "a","b","c","d","e","f","g","h","i","j","k", "l","m","n","o","p","q","r","s", - "t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde", - "Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis", - "aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla", - "eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex", - "idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde", - "uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent", - "sterling","section","bullet","paragraph","germandbls","registered", - "copyright","trademark","acute","dieresis","notequal","AE","Oslash", - "infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff", - "summation","product","pi","integral","ordfeminine","ordmasculine","Omega", - "ae","oslash","questiondown","exclamdown","logicalnot","radical","florin", - "approxequal","Delta","guillemotleft","guillemotright","ellipsis", - "nobreakspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash", - "quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge", - "ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright", - "fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase", - "perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave", - "Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple", - "Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde", - "macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron", - "Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth", - "Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior", - "twosuperior","threesuperior","onehalf","onequarter","threequarters","franc", - "Gbreve","gbreve","Idot","Scedilla","scedilla","Cacute","cacute","Ccaron", - "ccaron","dmacron","markingspace","capslock","shift","propeller","enter", - "markingtabrtol","markingtabltor","control","markingdeleteltor", - "markingdeletertol","option","escape","parbreakltor","parbreakrtol", - "newpage","checkmark","linebreakltor","linebreakrtol","markingnobreakspace", - "diamond","appleoutline" -}; - -/* -** This routine is called by the one below. -** It is also called from pprdrv_tt2.c -*/ -const char *ttfont_CharStrings_getname(struct TTFONT *font, int charindex) -{ - int GlyphIndex; - static char temp[80]; - char *ptr; - ULONG len; - - Fixed post_format; - - /* The 'post' table format number. */ - post_format = getFixed( font->post_table ); - - if ( post_format.whole != 2 || post_format.fraction != 0 ) - { - /* We don't have a glyph name table, so generate a name. - This generated name must match exactly the name that is - generated by FT2Font in get_glyph_name */ - PyOS_snprintf(temp, 80, "uni%08x", charindex); - return temp; - } - - GlyphIndex = (int)getUSHORT( font->post_table + 34 + (charindex * 2) ); - - if ( GlyphIndex <= 257 ) /* If a standard Apple name, */ - { - return Apple_CharStrings[GlyphIndex]; - } - else /* Otherwise, use one */ - { - /* of the pascal strings. */ - GlyphIndex -= 258; - - /* Set pointer to start of Pascal strings. */ - ptr = (char*)(font->post_table + 34 + (font->numGlyphs * 2)); - - len = (ULONG)*(ptr++); /* Step thru the strings */ - while (GlyphIndex--) /* until we get to the one */ - { - /* that we want. */ - ptr += len; - len = (ULONG)*(ptr++); - } - - if ( len >= sizeof(temp) ) - { - throw TTException("TrueType font file contains a very long PostScript name"); - } - - strncpy(temp,ptr,len); /* Copy the pascal string into */ - temp[len]='\0'; /* a buffer and make it ASCIIz. */ - - return temp; - } -} /* end of ttfont_CharStrings_getname() */ - -/* -** This is the central routine of this section. -*/ -void ttfont_CharStrings(TTStreamWriter& stream, struct TTFONT *font, std::vector& glyph_ids) -{ - Fixed post_format; - - /* The 'post' table format number. */ - post_format = getFixed( font->post_table ); - - /* Emmit the start of the PostScript code to define the dictionary. */ - stream.printf("/CharStrings %d dict dup begin\n", glyph_ids.size()+1); - /* Section 5.8.2 table 5.7 of the PS Language Ref says a CharStrings dictionary must contain an entry for .notdef */ - stream.printf("/.notdef 0 def\n"); - - /* Emmit one key-value pair for each glyph. */ - for (std::vector::const_iterator i = glyph_ids.begin(); - i != glyph_ids.end(); ++i) - { - if ((font->target_type == PS_TYPE_42 || - font->target_type == PS_TYPE_42_3_HYBRID) - && *i < 256) /* type 42 */ - { - stream.printf("/%s %d def\n",ttfont_CharStrings_getname(font, *i), *i); - } - else /* type 3 */ - { - stream.printf("/%s{",ttfont_CharStrings_getname(font, *i)); - - tt_type3_charproc(stream, font, *i); - - stream.putline("}_d"); /* "} bind def" */ - } - } - - stream.putline("end readonly def"); -} /* end of ttfont_CharStrings() */ - -/*---------------------------------------------------------------- -** Emmit the code to finish up the dictionary and turn -** it into a font. -----------------------------------------------------------------*/ -void ttfont_trailer(TTStreamWriter& stream, struct TTFONT *font) -{ - /* If we are generating a type 3 font, we need to provide */ - /* a BuildGlyph and BuildChar proceedures. */ - if (font->target_type == PS_TYPE_3 || - font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.put_char('\n'); - - stream.putline("/BuildGlyph"); - stream.putline(" {exch begin"); /* start font dictionary */ - stream.putline(" CharStrings exch"); - stream.putline(" 2 copy known not{pop /.notdef}if"); - stream.putline(" true 3 1 roll get exec"); - stream.putline(" end}_d"); - - stream.put_char('\n'); - - /* This proceedure is for compatibility with */ - /* level 1 interpreters. */ - stream.putline("/BuildChar {"); - stream.putline(" 1 index /Encoding get exch get"); - stream.putline(" 1 index /BuildGlyph get exec"); - stream.putline("}_d"); - - stream.put_char('\n'); - } - - /* If we are generating a type 42 font, we need to check to see */ - /* if this PostScript interpreter understands type 42 fonts. If */ - /* it doesn't, we will hope that the Apple TrueType rasterizer */ - /* has been loaded and we will adjust the font accordingly. */ - /* I found out how to do this by examining a TrueType font */ - /* generated by a Macintosh. That is where the TrueType interpreter */ - /* setup instructions and part of BuildGlyph came from. */ - if (font->target_type == PS_TYPE_42 || - font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.put_char('\n'); - - /* If we have no "resourcestatus" command, or FontType 42 */ - /* is unknown, leave "true" on the stack. */ - stream.putline("systemdict/resourcestatus known"); - stream.putline(" {42 /FontType resourcestatus"); - stream.putline(" {pop pop false}{true}ifelse}"); - stream.putline(" {true}ifelse"); - - /* If true, execute code to produce an error message if */ - /* we can't find Apple's TrueDict in VM. */ - stream.putline("{/TrueDict where{pop}{(%%[ Error: no TrueType rasterizer ]%%)= flush}ifelse"); - - /* Since we are expected to use Apple's TrueDict TrueType */ - /* reasterizer, change the font type to 3. */ - stream.putline("/FontType 3 def"); - - /* Define a string to hold the state of the Apple */ - /* TrueType interpreter. */ - stream.putline(" /TrueState 271 string def"); - - /* It looks like we get information about the resolution */ - /* of the printer and store it in the TrueState string. */ - stream.putline(" TrueDict begin sfnts save"); - stream.putline(" 72 0 matrix defaultmatrix dtransform dup"); - stream.putline(" mul exch dup mul add sqrt cvi 0 72 matrix"); - stream.putline(" defaultmatrix dtransform dup mul exch dup"); - stream.putline(" mul add sqrt cvi 3 -1 roll restore"); - stream.putline(" TrueState initer end"); - - /* This BuildGlyph procedure will look the name up in the */ - /* CharStrings array, and then check to see if what it gets */ - /* is a procedure. If it is, it executes it, otherwise, it */ - /* lets the TrueType rasterizer loose on it. */ - - /* When this proceedure is executed the stack contains */ - /* the font dictionary and the character name. We */ - /* exchange arguments and move the dictionary to the */ - /* dictionary stack. */ - stream.putline(" /BuildGlyph{exch begin"); - /* stack: charname */ - - /* Put two copies of CharStrings on the stack and consume */ - /* one testing to see if the charname is defined in it, */ - /* leave the answer on the stack. */ - stream.putline(" CharStrings dup 2 index known"); - /* stack: charname CharStrings bool */ - - /* Exchange the CharStrings dictionary and the charname, */ - /* but if the answer was false, replace the character name */ - /* with ".notdef". */ - stream.putline(" {exch}{exch pop /.notdef}ifelse"); - /* stack: CharStrings charname */ - - /* Get the value from the CharStrings dictionary and see */ - /* if it is executable. */ - stream.putline(" get dup xcheck"); - /* stack: CharStrings_entry */ - - /* If is a proceedure. Execute according to RBIIp 277-278. */ - stream.putline(" {currentdict systemdict begin begin exec end end}"); - - /* Is a TrueType character index, let the rasterizer at it. */ - stream.putline(" {TrueDict begin /bander load cvlit exch TrueState render end}"); - - stream.putline(" ifelse"); - - /* Pop the font's dictionary off the stack. */ - stream.putline(" end}bind def"); - - /* This is the level 1 compatibility BuildChar procedure. */ - /* See RBIIp 281. */ - stream.putline(" /BuildChar{"); - stream.putline(" 1 index /Encoding get exch get"); - stream.putline(" 1 index /BuildGlyph get exec"); - stream.putline(" }bind def"); - - /* Here we close the condition which is true */ - /* if the printer has no built-in TrueType */ - /* rasterizer. */ - stream.putline("}if"); - stream.put_char('\n'); - } /* end of if Type 42 not understood. */ - - stream.putline("FontName currentdict end definefont pop"); - /* stream.putline("%%EOF"); */ -} /* end of ttfont_trailer() */ - -/*------------------------------------------------------------------ -** This is the externally callable routine which inserts the font. -------------------------------------------------------------------*/ - -void read_font(const char *filename, font_type_enum target_type, std::vector& glyph_ids, TTFONT& font) -{ - BYTE *ptr; - - /* Decide what type of PostScript font we will be generating. */ - font.target_type = target_type; - - if (font.target_type == PS_TYPE_42) - { - bool has_low = false; - bool has_high = false; - - for (std::vector::const_iterator i = glyph_ids.begin(); - i != glyph_ids.end(); ++i) - { - if (*i > 255) - { - has_high = true; - if (has_low) break; - } - else - { - has_low = true; - if (has_high) break; - } - } - - if (has_high && has_low) - { - font.target_type = PS_TYPE_42_3_HYBRID; - } - else if (has_high && !has_low) - { - font.target_type = PS_TYPE_3; - } - } - - /* Save the file name for error messages. */ - font.filename=filename; - - /* Open the font file */ - if ( (font.file = fopen(filename,"rb")) == (FILE*)NULL ) - { - throw TTException("Failed to open TrueType font"); - } - - /* Allocate space for the unvarying part of the offset table. */ - assert(font.offset_table == NULL); - font.offset_table = (BYTE*)calloc( 12, sizeof(BYTE) ); - - /* Read the first part of the offset table. */ - if ( fread( font.offset_table, sizeof(BYTE), 12, font.file ) != 12 ) - { - throw TTException("TrueType font may be corrupt (reason 1)"); - } - - /* Determine how many directory entries there are. */ - font.numTables = getUSHORT( font.offset_table + 4 ); - debug("numTables=%d",(int)font.numTables); - - /* Expand the memory block to hold the whole thing. */ - font.offset_table = (BYTE*)realloc( font.offset_table, sizeof(BYTE) * (12 + font.numTables * 16) ); - - /* Read the rest of the table directory. */ - if ( fread( font.offset_table + 12, sizeof(BYTE), (font.numTables*16), font.file ) != (font.numTables*16) ) - { - throw TTException("TrueType font may be corrupt (reason 2)"); - } - - /* Extract information from the "Offset" table. */ - font.TTVersion = getFixed( font.offset_table ); - - /* Load the "head" table and extract information from it. */ - ptr = GetTable(&font, "head"); - try - { - font.MfrRevision = getFixed( ptr + 4 ); /* font revision number */ - font.unitsPerEm = getUSHORT( ptr + 18 ); - font.HUPM = font.unitsPerEm / 2; - debug("unitsPerEm=%d",(int)font.unitsPerEm); - font.llx = topost2( getFWord( ptr + 36 ) ); /* bounding box info */ - font.lly = topost2( getFWord( ptr + 38 ) ); - font.urx = topost2( getFWord( ptr + 40 ) ); - font.ury = topost2( getFWord( ptr + 42 ) ); - font.indexToLocFormat = getSHORT( ptr + 50 ); /* size of 'loca' data */ - if (font.indexToLocFormat != 0 && font.indexToLocFormat != 1) - { - throw TTException("TrueType font is unusable because indexToLocFormat != 0"); - } - if ( getSHORT(ptr+52) != 0 ) - { - throw TTException("TrueType font is unusable because glyphDataFormat != 0"); - } - } - catch (TTException& ) - { - free(ptr); - throw; - } - free(ptr); - - /* Load information from the "name" table. */ - Read_name(&font); - - /* We need to have the PostScript table around. */ - assert(font.post_table == NULL); - font.post_table = GetTable(&font, "post"); - font.numGlyphs = getUSHORT( font.post_table + 32 ); - - /* If we are generating a Type 3 font, we will need to */ - /* have the 'loca' and 'glyf' tables arround while */ - /* we are generating the CharStrings. */ - if (font.target_type == PS_TYPE_3 || font.target_type == PS_TYPE_42_3_HYBRID) - { - BYTE *ptr; /* We need only one value */ - ptr = GetTable(&font, "hhea"); - font.numberOfHMetrics = getUSHORT(ptr + 34); - free(ptr); - - assert(font.loca_table == NULL); - font.loca_table = GetTable(&font,"loca"); - assert(font.glyf_table == NULL); - font.glyf_table = GetTable(&font,"glyf"); - assert(font.hmtx_table == NULL); - font.hmtx_table = GetTable(&font,"hmtx"); - } - - if (glyph_ids.size() == 0) - { - glyph_ids.clear(); - glyph_ids.reserve(font.numGlyphs); - for (int x = 0; x < font.numGlyphs; ++x) - { - glyph_ids.push_back(x); - } - } - else if (font.target_type == PS_TYPE_3 || - font.target_type == PS_TYPE_42_3_HYBRID) - { - ttfont_add_glyph_dependencies(&font, glyph_ids); - } - -} /* end of insert_ttfont() */ - -void insert_ttfont(const char *filename, TTStreamWriter& stream, - font_type_enum target_type, std::vector& glyph_ids) -{ - struct TTFONT font; - - read_font(filename, target_type, glyph_ids, font); - - /* Write the header for the PostScript font. */ - ttfont_header(stream, &font); - - /* Define the encoding. */ - ttfont_encoding(stream, &font, glyph_ids, target_type); - - /* Insert FontInfo dictionary. */ - ttfont_FontInfo(stream, &font); - - /* If we are generating a type 42 font, */ - /* emmit the sfnts array. */ - if (font.target_type == PS_TYPE_42 || - font.target_type == PS_TYPE_42_3_HYBRID) - { - ttfont_sfnts(stream, &font); - } - - /* Emmit the CharStrings array. */ - ttfont_CharStrings(stream, &font, glyph_ids); - - /* Send the font trailer. */ - ttfont_trailer(stream, &font); - -} /* end of insert_ttfont() */ - -TTFONT::TTFONT() : - file(NULL), - PostName(NULL), - FullName(NULL), - FamilyName(NULL), - Style(NULL), - Copyright(NULL), - Version(NULL), - Trademark(NULL), - offset_table(NULL), - post_table(NULL), - loca_table(NULL), - glyf_table(NULL), - hmtx_table(NULL) -{ - -} - -TTFONT::~TTFONT() -{ - if (file) - { - fclose(file); - } - free(PostName); - free(FullName); - free(FamilyName); - free(Style); - free(Copyright); - free(Version); - free(Trademark); - free(offset_table); - free(post_table); - free(loca_table); - free(glyf_table); - free(hmtx_table); -} - -/* end of file */ diff --git a/extern/ttconv/pprdrv_tt2.cpp b/extern/ttconv/pprdrv_tt2.cpp deleted file mode 100644 index ec2298c8c42b..000000000000 --- a/extern/ttconv/pprdrv_tt2.cpp +++ /dev/null @@ -1,693 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* - * Modified for use within matplotlib - * 5 July 2007 - * Michael Droettboom - */ - -/* -** ~ppr/src/pprdrv/pprdrv_tt2.c -** Copyright 1995, Trinity College Computing Center. -** Written by David Chappell. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -** -** TrueType font support. These functions allow PPR to generate -** PostScript fonts from Microsoft compatible TrueType font files. -** -** The functions in this file do most of the work to convert a -** TrueType font to a type 3 PostScript font. -** -** Most of the material in this file is derived from a program called -** "ttf2ps" which L. S. Ng posted to the usenet news group -** "comp.sources.postscript". The author did not provide a copyright -** notice or indicate any restrictions on use. -** -** Last revised 11 July 1995. -*/ - -#include -#include -#include -#include -#include "pprdrv.h" -#include "truetype.h" -#include -#include -#include - -class GlyphToType3 -{ -private: - GlyphToType3& operator=(const GlyphToType3& other); - GlyphToType3(const GlyphToType3& other); - - /* The PostScript bounding box. */ - int llx,lly,urx,ury; - int advance_width; - - /* Variables to hold the character data. */ - int *epts_ctr; /* array of contour endpoints */ - int num_pts, num_ctr; /* number of points, number of coutours */ - FWord *xcoor, *ycoor; /* arrays of x and y coordinates */ - BYTE *tt_flags; /* array of TrueType flags */ - - int stack_depth; /* A book-keeping variable for keeping track of the depth of the PS stack */ - - void load_char(TTFONT* font, BYTE *glyph); - void stack(TTStreamWriter& stream, int new_elem); - void stack_end(TTStreamWriter& stream); - void PSConvert(TTStreamWriter& stream); - void PSCurveto(TTStreamWriter& stream, - FWord x0, FWord y0, - FWord x1, FWord y1, - FWord x2, FWord y2); - void PSMoveto(TTStreamWriter& stream, int x, int y); - void PSLineto(TTStreamWriter& stream, int x, int y); - void do_composite(TTStreamWriter& stream, struct TTFONT *font, BYTE *glyph); - -public: - GlyphToType3(TTStreamWriter& stream, struct TTFONT *font, int charindex, bool embedded = false); - ~GlyphToType3(); -}; - -// Each point on a TrueType contour is either on the path or off it (a -// control point); here's a simple representation for building such -// contours. Added by Jouni Seppänen 2012-05-27. -enum Flag { ON_PATH, OFF_PATH }; -struct FlaggedPoint -{ - enum Flag flag; - FWord x; - FWord y; - FlaggedPoint(Flag flag_, FWord x_, FWord y_): flag(flag_), x(x_), y(y_) {}; -}; - -/* -** This routine is used to break the character -** procedure up into a number of smaller -** procedures. This is necessary so as not to -** overflow the stack on certain level 1 interpreters. -** -** Prepare to push another item onto the stack, -** starting a new proceedure if necessary. -** -** Not all the stack depth calculations in this routine -** are perfectly accurate, but they do the job. -*/ -void GlyphToType3::stack(TTStreamWriter& stream, int new_elem) -{ - if ( num_pts > 25 ) /* Only do something of we will have a log of points. */ - { - if (stack_depth == 0) - { - stream.put_char('{'); - stack_depth=1; - } - - stack_depth += new_elem; /* Account for what we propose to add */ - - if (stack_depth > 100) - { - stream.puts("}_e{"); - stack_depth = 3 + new_elem; /* A rough estimate */ - } - } -} /* end of stack() */ - -void GlyphToType3::stack_end(TTStreamWriter& stream) /* called at end */ -{ - if ( stack_depth ) - { - stream.puts("}_e"); - stack_depth=0; - } -} /* end of stack_end() */ - -/* -** We call this routine to emmit the PostScript code -** for the character we have loaded with load_char(). -*/ -void GlyphToType3::PSConvert(TTStreamWriter& stream) -{ - int j, k; - - /* Step thru the contours. - * j = index to xcoor, ycoor, tt_flags (point data) - * k = index to epts_ctr (which points belong to the same contour) */ - for(j = k = 0; k < num_ctr; k++) - { - // A TrueType contour consists of on-path and off-path points. - // Two consecutive on-path points are to be joined with a - // line; off-path points between on-path points indicate a - // quadratic spline, where the off-path point is the control - // point. Two consecutive off-path points have an implicit - // on-path point midway between them. - std::list points; - - // Represent flags and x/y coordinates as a C++ list - for (; j <= epts_ctr[k]; j++) - { - if (!(tt_flags[j] & 1)) { - points.push_back(FlaggedPoint(OFF_PATH, xcoor[j], ycoor[j])); - } else { - points.push_back(FlaggedPoint(ON_PATH, xcoor[j], ycoor[j])); - } - } - - if (points.size() == 0) { - // Don't try to access the last element of an empty list - continue; - } - - // For any two consecutive off-path points, insert the implied - // on-path point. - FlaggedPoint prev = points.back(); - for (std::list::iterator it = points.begin(); - it != points.end(); - it++) - { - if (prev.flag == OFF_PATH && it->flag == OFF_PATH) - { - points.insert(it, - FlaggedPoint(ON_PATH, - (prev.x + it->x) / 2, - (prev.y + it->y) / 2)); - } - prev = *it; - } - // Handle the wrap-around: insert a point either at the beginning - // or at the end that has the same coordinates as the opposite point. - // This also ensures that the initial point is ON_PATH. - if (points.front().flag == OFF_PATH) - { - assert(points.back().flag == ON_PATH); - points.insert(points.begin(), points.back()); - } - else - { - assert(points.front().flag == ON_PATH); - points.push_back(points.front()); - } - - // The first point - stack(stream, 3); - PSMoveto(stream, points.front().x, points.front().y); - - // Step through the remaining points - std::list::const_iterator it = points.begin(); - for (it++; it != points.end(); /* incremented inside */) - { - const FlaggedPoint& point = *it; - if (point.flag == ON_PATH) - { - stack(stream, 3); - PSLineto(stream, point.x, point.y); - it++; - } else { - std::list::const_iterator prev = it, next = it; - prev--; - next++; - assert(prev->flag == ON_PATH); - assert(next->flag == ON_PATH); - stack(stream, 7); - PSCurveto(stream, - prev->x, prev->y, - point.x, point.y, - next->x, next->y); - it++; - it++; - } - } - } - - /* Now, we can fill the whole thing. */ - stack(stream, 1); - stream.puts("_cl"); -} /* end of PSConvert() */ - -void GlyphToType3::PSMoveto(TTStreamWriter& stream, int x, int y) -{ - stream.printf("%d %d _m\n", x, y); -} - -void GlyphToType3::PSLineto(TTStreamWriter& stream, int x, int y) -{ - stream.printf("%d %d _l\n", x, y); -} - -/* -** Emit a PostScript "curveto" command, assuming the current point -** is (x0, y0), the control point of a quadratic spline is (x1, y1), -** and the endpoint is (x2, y2). Note that this requires a conversion, -** since PostScript splines are cubic. -*/ -void GlyphToType3::PSCurveto(TTStreamWriter& stream, - FWord x0, FWord y0, - FWord x1, FWord y1, - FWord x2, FWord y2) -{ - double sx[3], sy[3], cx[3], cy[3]; - - sx[0] = x0; - sy[0] = y0; - sx[1] = x1; - sy[1] = y1; - sx[2] = x2; - sy[2] = y2; - cx[0] = (2*sx[1]+sx[0])/3; - cy[0] = (2*sy[1]+sy[0])/3; - cx[1] = (sx[2]+2*sx[1])/3; - cy[1] = (sy[2]+2*sy[1])/3; - cx[2] = sx[2]; - cy[2] = sy[2]; - stream.printf("%d %d %d %d %d %d _c\n", - (int)cx[0], (int)cy[0], (int)cx[1], (int)cy[1], - (int)cx[2], (int)cy[2]); -} - -/* -** Deallocate the structures which stored -** the data for the last simple glyph. -*/ -GlyphToType3::~GlyphToType3() -{ - free(tt_flags); /* The flags array */ - free(xcoor); /* The X coordinates */ - free(ycoor); /* The Y coordinates */ - free(epts_ctr); /* The array of contour endpoints */ -} - -/* -** Load the simple glyph data pointed to by glyph. -** The pointer "glyph" should point 10 bytes into -** the glyph data. -*/ -void GlyphToType3::load_char(TTFONT* font, BYTE *glyph) -{ - int x; - BYTE c, ct; - - /* Read the contour endpoints list. */ - epts_ctr = (int *)calloc(num_ctr,sizeof(int)); - for (x = 0; x < num_ctr; x++) - { - epts_ctr[x] = getUSHORT(glyph); - glyph += 2; - } - - /* From the endpoint of the last contour, we can */ - /* determine the number of points. */ - num_pts = epts_ctr[num_ctr-1]+1; -#ifdef DEBUG_TRUETYPE - debug("num_pts=%d",num_pts); - stream.printf("%% num_pts=%d\n",num_pts); -#endif - - /* Skip the instructions. */ - x = getUSHORT(glyph); - glyph += 2; - glyph += x; - - /* Allocate space to hold the data. */ - tt_flags = (BYTE *)calloc(num_pts,sizeof(BYTE)); - xcoor = (FWord *)calloc(num_pts,sizeof(FWord)); - ycoor = (FWord *)calloc(num_pts,sizeof(FWord)); - - /* Read the flags array, uncompressing it as we go. */ - /* There is danger of overflow here. */ - for (x = 0; x < num_pts; ) - { - tt_flags[x++] = c = *(glyph++); - - if (c&8) /* If next byte is repeat count, */ - { - ct = *(glyph++); - - if ( (x + ct) > num_pts ) - { - throw TTException("Error in TT flags"); - } - - while (ct--) - { - tt_flags[x++] = c; - } - } - } - - /* Read the x coordinates */ - for (x = 0; x < num_pts; x++) - { - if (tt_flags[x] & 2) /* one byte value with */ - { - /* external sign */ - c = *(glyph++); - xcoor[x] = (tt_flags[x] & 0x10) ? c : (-1 * (int)c); - } - else if (tt_flags[x] & 0x10) /* repeat last */ - { - xcoor[x] = 0; - } - else /* two byte signed value */ - { - xcoor[x] = getFWord(glyph); - glyph+=2; - } - } - - /* Read the y coordinates */ - for (x = 0; x < num_pts; x++) - { - if (tt_flags[x] & 4) /* one byte value with */ - { - /* external sign */ - c = *(glyph++); - ycoor[x] = (tt_flags[x] & 0x20) ? c : (-1 * (int)c); - } - else if (tt_flags[x] & 0x20) /* repeat last value */ - { - ycoor[x] = 0; - } - else /* two byte signed value */ - { - ycoor[x] = getUSHORT(glyph); - glyph+=2; - } - } - - /* Convert delta values to absolute values. */ - for (x = 1; x < num_pts; x++) - { - xcoor[x] += xcoor[x-1]; - ycoor[x] += ycoor[x-1]; - } - - for (x=0; x < num_pts; x++) - { - xcoor[x] = topost(xcoor[x]); - ycoor[x] = topost(ycoor[x]); - } - -} /* end of load_char() */ - -/* -** Emmit PostScript code for a composite character. -*/ -void GlyphToType3::do_composite(TTStreamWriter& stream, struct TTFONT *font, BYTE *glyph) -{ - USHORT flags; - USHORT glyphIndex; - int arg1; - int arg2; - - /* Once around this loop for each component. */ - do - { - flags = getUSHORT(glyph); /* read the flags word */ - glyph += 2; - - glyphIndex = getUSHORT(glyph); /* read the glyphindex word */ - glyph += 2; - - if (flags & ARG_1_AND_2_ARE_WORDS) - { - /* The tt spec. seems to say these are signed. */ - arg1 = getSHORT(glyph); - glyph += 2; - arg2 = getSHORT(glyph); - glyph += 2; - } - else /* The tt spec. does not clearly indicate */ - { - /* whether these values are signed or not. */ - arg1 = *(signed char *)(glyph++); - arg2 = *(signed char *)(glyph++); - } - - if (flags & WE_HAVE_A_SCALE) - { - glyph += 2; - } - else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) - { - glyph += 4; - } - else if (flags & WE_HAVE_A_TWO_BY_TWO) - { - glyph += 8; - } - else - { - } - - /* Debugging */ -#ifdef DEBUG_TRUETYPE - stream.printf("%% flags=%d, arg1=%d, arg2=%d\n", - (int)flags,arg1,arg2); -#endif - - /* If we have an (X,Y) shift and it is non-zero, */ - /* translate the coordinate system. */ - if ( flags & ARGS_ARE_XY_VALUES ) - { - if ( arg1 != 0 || arg2 != 0 ) - stream.printf("gsave %d %d translate\n", topost(arg1), topost(arg2) ); - } - else - { - stream.printf("%% unimplemented shift, arg1=%d, arg2=%d\n",arg1,arg2); - } - - /* Invoke the CharStrings procedure to print the component. */ - stream.printf("false CharStrings /%s get exec\n", - ttfont_CharStrings_getname(font, glyphIndex)); - - /* If we translated the coordinate system, */ - /* put it back the way it was. */ - if ( flags & ARGS_ARE_XY_VALUES && (arg1 != 0 || arg2 != 0) ) - { - stream.puts("grestore "); - } - - } - while (flags & MORE_COMPONENTS); - -} /* end of do_composite() */ - -/* -** Return a pointer to a specific glyph's data. -*/ -BYTE *find_glyph_data(struct TTFONT *font, int charindex) -{ - ULONG off; - ULONG length; - - /* Read the glyph offset from the index to location table. */ - if (font->indexToLocFormat == 0) - { - off = getUSHORT( font->loca_table + (charindex * 2) ); - off *= 2; - length = getUSHORT( font->loca_table + ((charindex+1) * 2) ); - length *= 2; - length -= off; - } - else - { - off = getULONG( font->loca_table + (charindex * 4) ); - length = getULONG( font->loca_table + ((charindex+1) * 4) ); - length -= off; - } - - if (length > 0) - { - return font->glyf_table + off; - } - else - { - return (BYTE*)NULL; - } - -} /* end of find_glyph_data() */ - -GlyphToType3::GlyphToType3(TTStreamWriter& stream, struct TTFONT *font, int charindex, bool embedded /* = false */) -{ - BYTE *glyph; - - tt_flags = NULL; - xcoor = NULL; - ycoor = NULL; - epts_ctr = NULL; - stack_depth = 0; - - /* Get a pointer to the data. */ - glyph = find_glyph_data( font, charindex ); - - /* If the character is blank, it has no bounding box, */ - /* otherwise read the bounding box. */ - if ( glyph == (BYTE*)NULL ) - { - llx=lly=urx=ury=0; /* A blank char has an all zero BoundingBox */ - num_ctr=0; /* Set this for later if()s */ - } - else - { - /* Read the number of contours. */ - num_ctr = getSHORT(glyph); - - /* Read PostScript bounding box. */ - llx = getFWord(glyph + 2); - lly = getFWord(glyph + 4); - urx = getFWord(glyph + 6); - ury = getFWord(glyph + 8); - - /* Advance the pointer. */ - glyph += 10; - } - - /* If it is a simple character, load its data. */ - if (num_ctr > 0) - { - load_char(font, glyph); - } - else - { - num_pts=0; - } - - /* Consult the horizontal metrics table to determine */ - /* the character width. */ - if ( charindex < font->numberOfHMetrics ) - { - advance_width = getuFWord( font->hmtx_table + (charindex * 4) ); - } - else - { - advance_width = getuFWord( font->hmtx_table + ((font->numberOfHMetrics-1) * 4) ); - } - - /* Execute setcachedevice in order to inform the font machinery */ - /* of the character bounding box and advance width. */ - stack(stream, 7); - if (font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.printf("pop gsave .001 .001 scale %d 0 %d %d %d %d setcachedevice\n", - topost(advance_width), - topost(llx), topost(lly), topost(urx), topost(ury) ); - } - else - { - stream.printf("%d 0 %d %d %d %d _sc\n", - topost(advance_width), - topost(llx), topost(lly), topost(urx), topost(ury) ); - } - - /* If it is a simple glyph, convert it, */ - /* otherwise, close the stack business. */ - if ( num_ctr > 0 ) /* simple */ - { - PSConvert(stream); - } - else if ( num_ctr < 0 ) /* composite */ - { - do_composite(stream, font, glyph); - } - - if (font->target_type == PS_TYPE_42_3_HYBRID) - { - stream.printf("\ngrestore\n"); - } - - stack_end(stream); -} - -/* -** This is the routine which is called from pprdrv_tt.c. -*/ -void tt_type3_charproc(TTStreamWriter& stream, struct TTFONT *font, int charindex) -{ - GlyphToType3 glyph(stream, font, charindex); -} /* end of tt_type3_charproc() */ - -/* -** Some of the given glyph ids may refer to composite glyphs. -** This function adds all of the dependencies of those composite -** glyphs to the glyph id vector. Michael Droettboom [06-07-07] -*/ -void ttfont_add_glyph_dependencies(struct TTFONT *font, std::vector& glyph_ids) -{ - std::sort(glyph_ids.begin(), glyph_ids.end()); - - std::stack glyph_stack; - for (std::vector::iterator i = glyph_ids.begin(); - i != glyph_ids.end(); ++i) - { - glyph_stack.push(*i); - } - - while (glyph_stack.size()) - { - int gind = glyph_stack.top(); - glyph_stack.pop(); - - BYTE* glyph = find_glyph_data( font, gind ); - if (glyph != (BYTE*)NULL) - { - - int num_ctr = getSHORT(glyph); - if (num_ctr <= 0) // This is a composite glyph - { - - glyph += 10; - USHORT flags = 0; - - do - { - flags = getUSHORT(glyph); - glyph += 2; - gind = (int)getUSHORT(glyph); - glyph += 2; - - std::vector::iterator insertion = - std::lower_bound(glyph_ids.begin(), glyph_ids.end(), gind); - if (insertion == glyph_ids.end() || *insertion != gind) - { - glyph_ids.insert(insertion, gind); - glyph_stack.push(gind); - } - - if (flags & ARG_1_AND_2_ARE_WORDS) - { - glyph += 4; - } - else - { - glyph += 2; - } - - if (flags & WE_HAVE_A_SCALE) - { - glyph += 2; - } - else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) - { - glyph += 4; - } - else if (flags & WE_HAVE_A_TWO_BY_TWO) - { - glyph += 8; - } - } - while (flags & MORE_COMPONENTS); - } - } - } -} - -/* end of file */ diff --git a/extern/ttconv/truetype.h b/extern/ttconv/truetype.h deleted file mode 100644 index 86be14fe3705..000000000000 --- a/extern/ttconv/truetype.h +++ /dev/null @@ -1,129 +0,0 @@ -/* -*- mode: c; c-basic-offset: 4 -*- */ - -/* - * Modified for use within matplotlib - * 5 July 2007 - * Michael Droettboom - */ - -#include - -/* -** ~ppr/src/include/typetype.h -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -** -** This include file is shared by the source files -** "pprdrv/pprdrv_tt.c" and "pprdrv/pprdrv_tt2.c". -** -** Last modified 19 April 1995. -*/ - -/* Types used in TrueType font files. */ -#define BYTE unsigned char -#define USHORT unsigned short int -#define SHORT short signed int -#define ULONG unsigned int -#define FIXED long signed int -#define FWord short signed int -#define uFWord short unsigned int - -/* This structure stores a 16.16 bit fixed */ -/* point number. */ -typedef struct - { - short int whole; - unsigned short int fraction; - } Fixed; - -/* This structure tells what we have found out about */ -/* the current font. */ -struct TTFONT - { - // A quick-and-dirty way to create a minimum level of exception safety - // Added by Michael Droettboom - TTFONT(); - ~TTFONT(); - - const char *filename; /* Name of TT file */ - FILE *file; /* the open TT file */ - font_type_enum target_type; /* 42 or 3 for PS, or -3 for PDF */ - - ULONG numTables; /* number of tables present */ - char *PostName; /* Font's PostScript name */ - char *FullName; /* Font's full name */ - char *FamilyName; /* Font's family name */ - char *Style; /* Font's style string */ - char *Copyright; /* Font's copyright string */ - char *Version; /* Font's version string */ - char *Trademark; /* Font's trademark string */ - int llx,lly,urx,ury; /* bounding box */ - - Fixed TTVersion; /* Truetype version number from offset table */ - Fixed MfrRevision; /* Revision number of this font */ - - BYTE *offset_table; /* Offset table in memory */ - BYTE *post_table; /* 'post' table in memory */ - - BYTE *loca_table; /* 'loca' table in memory */ - BYTE *glyf_table; /* 'glyf' table in memory */ - BYTE *hmtx_table; /* 'hmtx' table in memory */ - - USHORT numberOfHMetrics; - int unitsPerEm; /* unitsPerEm converted to int */ - int HUPM; /* half of above */ - - int numGlyphs; /* from 'post' table */ - - int indexToLocFormat; /* short or long offsets */ -}; - -ULONG getULONG(BYTE *p); -USHORT getUSHORT(BYTE *p); -Fixed getFixed(BYTE *p); - -/* -** Get an funits word. -** since it is 16 bits long, we can -** use getUSHORT() to do the real work. -*/ -#define getFWord(x) (FWord)getUSHORT(x) -#define getuFWord(x) (uFWord)getUSHORT(x) - -/* -** We can get a SHORT by making USHORT signed. -*/ -#define getSHORT(x) (SHORT)getUSHORT(x) - -/* This is the one routine in pprdrv_tt.c that is */ -/* called from pprdrv_tt.c. */ -const char *ttfont_CharStrings_getname(struct TTFONT *font, int charindex); - -void tt_type3_charproc(TTStreamWriter& stream, struct TTFONT *font, int charindex); - -/* Added 06-07-07 Michael Droettboom */ -void ttfont_add_glyph_dependencies(struct TTFONT *font, std::vector& glypy_ids); - -/* This routine converts a number in the font's character coordinate */ -/* system to a number in a 1000 unit character system. */ -#define topost(x) (int)( ((int)(x) * 1000 + font->HUPM) / font->unitsPerEm ) -#define topost2(x) (int)( ((int)(x) * 1000 + font.HUPM) / font.unitsPerEm ) - -/* Composite glyph values. */ -#define ARG_1_AND_2_ARE_WORDS 1 -#define ARGS_ARE_XY_VALUES 2 -#define ROUND_XY_TO_GRID 4 -#define WE_HAVE_A_SCALE 8 -/* RESERVED 16 */ -#define MORE_COMPONENTS 32 -#define WE_HAVE_AN_X_AND_Y_SCALE 64 -#define WE_HAVE_A_TWO_BY_TWO 128 -#define WE_HAVE_INSTRUCTIONS 256 -#define USE_MY_METRICS 512 - -/* end of file */ diff --git a/extern/ttconv/ttutil.cpp b/extern/ttconv/ttutil.cpp deleted file mode 100644 index 6028e1d45d4a..000000000000 --- a/extern/ttconv/ttutil.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* - * Modified for use within matplotlib - * 5 July 2007 - * Michael Droettboom - */ - -/* Very simple interface to the ppr TT routines */ -/* (c) Frank Siegert 1996 */ - -#include -#include -#include -#include "pprdrv.h" - -#define PRINTF_BUFFER_SIZE 512 -void TTStreamWriter::printf(const char* format, ...) -{ - va_list arg_list; - va_start(arg_list, format); - char buffer[PRINTF_BUFFER_SIZE]; - -#if defined(WIN32) || defined(_MSC_VER) - int size = _vsnprintf(buffer, PRINTF_BUFFER_SIZE, format, arg_list); -#else - int size = vsnprintf(buffer, PRINTF_BUFFER_SIZE, format, arg_list); -#endif - if (size >= PRINTF_BUFFER_SIZE) { - char* buffer2 = (char*)malloc(size); -#if defined(WIN32) || defined(_MSC_VER) - _vsnprintf(buffer2, size, format, arg_list); -#else - vsnprintf(buffer2, size, format, arg_list); -#endif - this->write(buffer2); - free(buffer2); - } else { - this->write(buffer); - } - - va_end(arg_list); -} - -void TTStreamWriter::put_char(int val) -{ - char c[2]; - c[0] = (char)val; - c[1] = 0; - this->write(c); -} - -void TTStreamWriter::puts(const char *a) -{ - this->write(a); -} - -void TTStreamWriter::putline(const char *a) -{ - this->write(a); - this->write("\n"); -} - -void replace_newlines_with_spaces(char *a) { - char* i = a; - while (*i != 0) { - if (*i == '\r' || *i == '\n') - *i = ' '; - i++; - } -} diff --git a/galleries/examples/animation/bayes_update.py b/galleries/examples/animation/bayes_update.py index 1081b4704623..6d36bd1e6149 100644 --- a/galleries/examples/animation/bayes_update.py +++ b/galleries/examples/animation/bayes_update.py @@ -69,3 +69,6 @@ def __call__(self, i): ud = UpdateDist(ax, prob=0.7) anim = FuncAnimation(fig, ud, init_func=ud.start, frames=100, interval=100, blit=True) plt.show() + +# %% +# .. tags:: animation, plot-type: line diff --git a/galleries/examples/animation/dynamic_image.py b/galleries/examples/animation/dynamic_image.py index 541edede31e4..221f6f08d0c8 100644 --- a/galleries/examples/animation/dynamic_image.py +++ b/galleries/examples/animation/dynamic_image.py @@ -46,3 +46,6 @@ def f(x, y): # ani.save("movie.mp4", writer=writer) plt.show() + +# %% +# .. tags:: animation diff --git a/galleries/examples/animation/pause_resume.py b/galleries/examples/animation/pause_resume.py index 7b1fade30322..e62dd049e11f 100644 --- a/galleries/examples/animation/pause_resume.py +++ b/galleries/examples/animation/pause_resume.py @@ -1,7 +1,7 @@ """ -================================= -Pausing and Resuming an Animation -================================= +============================= +Pause and resume an animation +============================= This example showcases: diff --git a/galleries/examples/animation/random_walk.py b/galleries/examples/animation/random_walk.py index 4be0b461f933..9dd4383fd548 100644 --- a/galleries/examples/animation/random_walk.py +++ b/galleries/examples/animation/random_walk.py @@ -50,3 +50,6 @@ def update_lines(num, walks, lines): fig, update_lines, num_steps, fargs=(walks, lines), interval=100) plt.show() + +# %% +# .. tags:: animation, plot-type: 3D diff --git a/galleries/examples/animation/simple_scatter.py b/galleries/examples/animation/simple_scatter.py index a006f73bab4c..3f8c285810a3 100644 --- a/galleries/examples/animation/simple_scatter.py +++ b/galleries/examples/animation/simple_scatter.py @@ -19,10 +19,10 @@ def animate(i): scat.set_offsets((x[i], 0)) - return scat, + return (scat,) -ani = animation.FuncAnimation(fig, animate, repeat=True, - frames=len(x) - 1, interval=50) + +ani = animation.FuncAnimation(fig, animate, repeat=True, frames=len(x) - 1, interval=50) # To save the animation using Pillow as a gif # writer = animation.PillowWriter(fps=15, @@ -31,3 +31,11 @@ def animate(i): # ani.save('scatter.gif', writer=writer) plt.show() + +# %% +# +# .. tags:: +# component: animation, +# plot-type: scatter, +# purpose: reference, +# level: intermediate diff --git a/galleries/examples/animation/strip_chart.py b/galleries/examples/animation/strip_chart.py index 919624c59652..0e533a255f1c 100644 --- a/galleries/examples/animation/strip_chart.py +++ b/galleries/examples/animation/strip_chart.py @@ -67,3 +67,5 @@ def emitter(p=0.1): blit=True, save_count=100) plt.show() + +# ..tags:: animation, plot-type: line diff --git a/galleries/examples/animation/unchained.py b/galleries/examples/animation/unchained.py index e93ed03ff99e..4c49d80bba81 100644 --- a/galleries/examples/animation/unchained.py +++ b/galleries/examples/animation/unchained.py @@ -1,7 +1,7 @@ """ -======================== -MATPLOTLIB **UNCHAINED** -======================== +==================== +Matplotlib unchained +==================== Comparative path demonstration of frequency from a fake signal of a pulsar (mostly known because of the cover for Joy Division's Unknown Pleasures). diff --git a/galleries/examples/axes_grid1/demo_axes_rgb.py b/galleries/examples/axes_grid1/demo_axes_rgb.py index ecbe1b89fd72..2cdb41fc323b 100644 --- a/galleries/examples/axes_grid1/demo_axes_rgb.py +++ b/galleries/examples/axes_grid1/demo_axes_rgb.py @@ -1,7 +1,7 @@ """ -================================== -Showing RGB channels using RGBAxes -================================== +=============================== +Show RGB channels using RGBAxes +=============================== `~.axes_grid1.axes_rgb.RGBAxes` creates a layout of 4 Axes for displaying RGB channels: one large Axes for the RGB image and 3 smaller Axes for the R, G, B 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 d989fb44bbab..f62a0f58e3bc 100644 --- a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py +++ b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py @@ -1,9 +1,9 @@ """ .. _demo-colorbar-with-inset-locator: -============================================================== -Controlling the position and size of colorbars with Inset Axes -============================================================== +=========================================================== +Control the position and size of a colorbar with Inset Axes +=========================================================== This example shows how to control the position, height, and width of colorbars using `~mpl_toolkits.axes_grid1.inset_locator.inset_axes`. diff --git a/galleries/examples/axes_grid1/demo_imagegrid_aspect.py b/galleries/examples/axes_grid1/demo_imagegrid_aspect.py index 820a2e8e1d2d..55268c41c9b1 100644 --- a/galleries/examples/axes_grid1/demo_imagegrid_aspect.py +++ b/galleries/examples/axes_grid1/demo_imagegrid_aspect.py @@ -1,6 +1,6 @@ """ ========================================= -Setting a fixed aspect on ImageGrid cells +ImageGrid cells with a fixed aspect ratio ========================================= """ diff --git a/galleries/examples/axes_grid1/inset_locator_demo.py b/galleries/examples/axes_grid1/inset_locator_demo.py index fa9c4593d932..e4b310ac6c73 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo.py +++ b/galleries/examples/axes_grid1/inset_locator_demo.py @@ -20,21 +20,21 @@ fig, (ax, ax2) = plt.subplots(1, 2, figsize=[5.5, 2.8]) # Create inset of width 1.3 inches and height 0.9 inches -# at the default upper right location +# at the default upper right location. axins = inset_axes(ax, width=1.3, height=0.9) # Create inset of width 30% and height 40% of the parent Axes' bounding box -# at the lower left corner (loc=3) -axins2 = inset_axes(ax, width="30%", height="40%", loc=3) +# at the lower left corner. +axins2 = inset_axes(ax, width="30%", height="40%", loc="lower left") # Create inset of mixed specifications in the second subplot; # width is 30% of parent Axes' bounding box and -# height is 1 inch at the upper left corner (loc=2) -axins3 = inset_axes(ax2, width="30%", height=1., loc=2) +# height is 1 inch at the upper left corner. +axins3 = inset_axes(ax2, width="30%", height=1., loc="upper left") -# Create an inset in the lower right corner (loc=4) with borderpad=1, i.e. -# 10 points padding (as 10pt is the default fontsize) to the parent Axes -axins4 = inset_axes(ax2, width="20%", height="20%", loc=4, borderpad=1) +# Create an inset in the lower right corner with borderpad=1, i.e. +# 10 points padding (as 10pt is the default fontsize) to the parent Axes. +axins4 = inset_axes(ax2, width="20%", height="20%", loc="lower right", borderpad=1) # Turn ticklabels of insets off for axi in [axins, axins2, axins3, axins4]: @@ -61,12 +61,12 @@ # in those coordinates. # Inside this bounding box an inset of half the bounding box' width and # three quarters of the bounding box' height is created. The lower left corner -# of the inset is aligned to the lower left corner of the bounding box (loc=3). +# of the inset is aligned to the lower left corner of the bounding box. # The inset is then offset by the default 0.5 in units of the font size. axins = inset_axes(ax, width="50%", height="75%", bbox_to_anchor=(.2, .4, .6, .5), - bbox_transform=ax.transAxes, loc=3) + bbox_transform=ax.transAxes, loc="lower left") # For visualization purposes we mark the bounding box by a rectangle ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="none", @@ -113,7 +113,7 @@ # Create an inset outside the Axes axins = inset_axes(ax, width="100%", height="100%", bbox_to_anchor=(1.05, .6, .5, .4), - bbox_transform=ax.transAxes, loc=2, borderpad=0) + bbox_transform=ax.transAxes, loc="upper left", borderpad=0) axins.tick_params(left=False, right=True, labelleft=False, labelright=True) # Create an inset with a 2-tuple bounding box. Note that this creates a @@ -121,7 +121,7 @@ # width and height in absolute units (inches). axins2 = inset_axes(ax, width=0.5, height=0.4, bbox_to_anchor=(0.33, 0.25), - bbox_transform=ax.transAxes, loc=3, borderpad=0) + bbox_transform=ax.transAxes, loc="lower left", borderpad=0) ax2 = fig.add_subplot(133) @@ -131,7 +131,7 @@ # Create inset in data coordinates using ax.transData as transform axins3 = inset_axes(ax2, width="100%", height="100%", bbox_to_anchor=(1e-2, 2, 1e3, 3), - bbox_transform=ax2.transData, loc=2, borderpad=0) + bbox_transform=ax2.transData, loc="upper left", borderpad=0) # Create an inset horizontally centered in figure coordinates and vertically # bound to line up with the Axes. @@ -140,6 +140,6 @@ transform = blended_transform_factory(fig.transFigure, ax2.transAxes) axins4 = inset_axes(ax2, width="16%", height="34%", bbox_to_anchor=(0, 0, 1, 1), - bbox_transform=transform, loc=8, borderpad=0) + bbox_transform=transform, loc="lower center", borderpad=0) plt.show() diff --git a/galleries/examples/axes_grid1/inset_locator_demo2.py b/galleries/examples/axes_grid1/inset_locator_demo2.py index f648c38e8d55..1bbbdd39b886 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo2.py +++ b/galleries/examples/axes_grid1/inset_locator_demo2.py @@ -36,7 +36,7 @@ def add_sizebar(ax, size): asb = AnchoredSizeBar(ax.transData, size, str(size), - loc=8, + loc="lower center", pad=0.1, borderpad=0.5, sep=5, frameon=False) ax.add_artist(asb) @@ -54,7 +54,7 @@ def add_sizebar(ax, size): ax2.imshow(Z2, extent=extent, origin="lower") -axins2 = zoomed_inset_axes(ax2, zoom=6, loc=1) +axins2 = zoomed_inset_axes(ax2, zoom=6, loc="upper right") axins2.imshow(Z2, extent=extent, origin="lower") # subregion of the original image diff --git a/galleries/examples/axes_grid1/parasite_simple.py b/galleries/examples/axes_grid1/parasite_simple.py index ad4922308a3f..a0c4d68051a9 100644 --- a/galleries/examples/axes_grid1/parasite_simple.py +++ b/galleries/examples/axes_grid1/parasite_simple.py @@ -20,7 +20,7 @@ host.legend(labelcolor="linecolor") -host.yaxis.get_label().set_color(p1.get_color()) -par.yaxis.get_label().set_color(p2.get_color()) +host.yaxis.label.set_color(p1.get_color()) +par.yaxis.label.set_color(p2.get_color()) plt.show() diff --git a/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py b/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py index e5ff19d9ee08..3f9bc4305b3f 100644 --- a/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py +++ b/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py @@ -1,7 +1,7 @@ """ -================================== -Scatter Histogram (Locatable Axes) -================================== +==================================================== +Align histogram to scatter plot using locatable Axes +==================================================== Show the marginal distributions of a scatter plot as histograms at the sides of the plot. diff --git a/galleries/examples/axisartist/simple_axis_pad.py b/galleries/examples/axisartist/simple_axis_pad.py index 9c613c820b2b..95f30ce1ffbc 100644 --- a/galleries/examples/axisartist/simple_axis_pad.py +++ b/galleries/examples/axisartist/simple_axis_pad.py @@ -1,6 +1,6 @@ """ =============== -Simple Axis Pad +Simple axis pad =============== """ diff --git a/galleries/examples/color/color_by_yvalue.py b/galleries/examples/color/color_by_yvalue.py index c9bee252aec4..193f840db39e 100644 --- a/galleries/examples/color/color_by_yvalue.py +++ b/galleries/examples/color/color_by_yvalue.py @@ -30,3 +30,10 @@ # in this example: # # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` +# +# .. tags:: +# +# styling: color +# styling: conditional +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/color_cycle_default.py b/galleries/examples/color/color_cycle_default.py index a41ff5f63ff8..af35f6d00f9e 100644 --- a/galleries/examples/color/color_cycle_default.py +++ b/galleries/examples/color/color_cycle_default.py @@ -9,33 +9,28 @@ import matplotlib.pyplot as plt import numpy as np -prop_cycle = plt.rcParams['axes.prop_cycle'] -colors = prop_cycle.by_key()['color'] +from matplotlib.colors import TABLEAU_COLORS, same_color -lwbase = plt.rcParams['lines.linewidth'] -thin = lwbase / 2 -thick = lwbase * 3 -fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) -for icol in range(2): - if icol == 0: - lwx, lwy = thin, lwbase - else: - lwx, lwy = lwbase, thick - for irow in range(2): - for i, color in enumerate(colors): - axs[irow, icol].axhline(i, color=color, lw=lwx) - axs[irow, icol].axvline(i, color=color, lw=lwy) +def f(x, a): + """A nice sigmoid-like parametrized curve, ending approximately at *a*.""" + return 0.85 * a * (1 / (1 + np.exp(-x)) + 0.2) - axs[1, icol].set_facecolor('k') - axs[1, icol].xaxis.set_ticks(np.arange(0, 10, 2)) - axs[0, icol].set_title(f'line widths (pts): {lwx:g}, {lwy:g}', - fontsize='medium') -for irow in range(2): - axs[irow, 0].yaxis.set_ticks(np.arange(0, 10, 2)) +fig, ax = plt.subplots() +ax.axis('off') +ax.set_title("Colors in the default property cycle") -fig.suptitle('Colors in the default prop_cycle', fontsize='large') +prop_cycle = plt.rcParams['axes.prop_cycle'] +colors = prop_cycle.by_key()['color'] +x = np.linspace(-4, 4, 200) + +for i, (color, color_name) in enumerate(zip(colors, TABLEAU_COLORS)): + assert same_color(color, color_name) + pos = 4.5 - i + ax.plot(x, f(x, pos)) + ax.text(4.2, pos, f"'C{i}': '{color_name}'", color=color, va="center") + ax.bar(9, 1, width=1.5, bottom=pos-0.5) plt.show() @@ -46,7 +41,14 @@ # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.axhline` / `matplotlib.pyplot.axhline` -# - `matplotlib.axes.Axes.axvline` / `matplotlib.pyplot.axvline` -# - `matplotlib.axes.Axes.set_facecolor` -# - `matplotlib.figure.Figure.suptitle` +# - `matplotlib.axes.Axes.axis` +# - `matplotlib.axes.Axes.text` +# - `matplotlib.colors.same_color` +# - `cycler.Cycler` +# +# .. tags:: +# +# styling: color +# purpose: reference +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/color_demo.py b/galleries/examples/color/color_demo.py index 8c4b7756cc3e..6822efc3faa7 100644 --- a/galleries/examples/color/color_demo.py +++ b/galleries/examples/color/color_demo.py @@ -75,3 +75,9 @@ # - `matplotlib.axes.Axes.set_xlabel` # - `matplotlib.axes.Axes.set_ylabel` # - `matplotlib.axes.Axes.tick_params` +# +# .. tags:: +# +# styling: color +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/colorbar_basics.py b/galleries/examples/color/colorbar_basics.py index 506789916637..8a35f8ac2b68 100644 --- a/galleries/examples/color/colorbar_basics.py +++ b/galleries/examples/color/colorbar_basics.py @@ -56,3 +56,10 @@ # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colorbar.Colorbar.minorticks_on` # - `matplotlib.colorbar.Colorbar.minorticks_off` +# +# .. tags:: +# +# component: colorbar +# styling: color +# plot-type: imshow +# level: beginner diff --git a/galleries/examples/color/colormap_reference.py b/galleries/examples/color/colormap_reference.py index ee01d7432b37..6f550161f2e9 100644 --- a/galleries/examples/color/colormap_reference.py +++ b/galleries/examples/color/colormap_reference.py @@ -29,7 +29,8 @@ 'hot', 'afmhot', 'gist_heat', 'copper']), ('Diverging', [ 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', - 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']), + 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', + 'berlin', 'managua', 'vanimo']), ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), ('Qualitative', [ 'Pastel1', 'Pastel2', 'Paired', 'Accent', @@ -94,3 +95,8 @@ def plot_color_gradients(cmap_category, cmap_list): # - `matplotlib.axes.Axes.imshow` # - `matplotlib.figure.Figure.text` # - `matplotlib.axes.Axes.set_axis_off` +# +# .. tags:: +# +# styling: colormap +# purpose: reference diff --git a/galleries/examples/color/custom_cmap.py b/galleries/examples/color/custom_cmap.py index 667dc3133819..616ab9f279fd 100644 --- a/galleries/examples/color/custom_cmap.py +++ b/galleries/examples/color/custom_cmap.py @@ -1,7 +1,7 @@ """ -========================================= -Creating a colormap from a list of colors -========================================= +======================================= +Create a colormap from a list of colors +======================================= For more detail on creating and manipulating colormaps see :ref:`colormap-manipulation`. @@ -280,3 +280,9 @@ # - `matplotlib.cm` # - `matplotlib.cm.ScalarMappable.set_cmap` # - `matplotlib.cm.ColormapRegistry.register` +# +# .. tags:: +# +# styling: colormap +# plot-type: imshow +# level: intermediate diff --git a/galleries/examples/color/individual_colors_from_cmap.py b/galleries/examples/color/individual_colors_from_cmap.py index 1a14bd6b2ae1..cdd176eb3be1 100644 --- a/galleries/examples/color/individual_colors_from_cmap.py +++ b/galleries/examples/color/individual_colors_from_cmap.py @@ -63,3 +63,10 @@ # # - `matplotlib.colors.Colormap` # - `matplotlib.colors.Colormap.resampled` +# +# .. tags:: +# +# component: colormap +# styling: color +# plot-type: line +# level: intermediate diff --git a/galleries/examples/color/named_colors.py b/galleries/examples/color/named_colors.py index d9a7259da773..a5bcf00cb0cb 100644 --- a/galleries/examples/color/named_colors.py +++ b/galleries/examples/color/named_colors.py @@ -121,3 +121,8 @@ def plot_colortable(colors, *, ncols=4, sort_colors=True): # - `matplotlib.figure.Figure.subplots_adjust` # - `matplotlib.axes.Axes.text` # - `matplotlib.patches.Rectangle` +# +# .. tags:: +# +# styling: color +# purpose: reference diff --git a/galleries/examples/color/set_alpha.py b/galleries/examples/color/set_alpha.py index 4130fe1109ef..b8ba559f5f4a 100644 --- a/galleries/examples/color/set_alpha.py +++ b/galleries/examples/color/set_alpha.py @@ -51,3 +51,9 @@ # # - `matplotlib.axes.Axes.bar` # - `matplotlib.pyplot.subplots` +# +# .. tags:: +# +# styling: color +# plot-type: bar +# level: beginner diff --git a/galleries/examples/event_handling/close_event.py b/galleries/examples/event_handling/close_event.py index 24b45b74ea48..060388269c8c 100644 --- a/galleries/examples/event_handling/close_event.py +++ b/galleries/examples/event_handling/close_event.py @@ -1,6 +1,6 @@ """ =========== -Close Event +Close event =========== Example to show connecting events that occur when the figure closes. diff --git a/galleries/examples/event_handling/looking_glass.py b/galleries/examples/event_handling/looking_glass.py index 6032b39b5b9e..a2a5f396c75a 100644 --- a/galleries/examples/event_handling/looking_glass.py +++ b/galleries/examples/event_handling/looking_glass.py @@ -1,6 +1,6 @@ """ ============= -Looking Glass +Looking glass ============= Example using mouse events to simulate a looking glass for inspecting data. diff --git a/galleries/examples/event_handling/poly_editor.py b/galleries/examples/event_handling/poly_editor.py index 5465cca0ed94..f6efd8bb8446 100644 --- a/galleries/examples/event_handling/poly_editor.py +++ b/galleries/examples/event_handling/poly_editor.py @@ -1,7 +1,7 @@ """ -=========== -Poly Editor -=========== +============== +Polygon editor +============== This is an example to show how to build cross-GUI applications using Matplotlib event handling to interact with objects on the canvas. diff --git a/galleries/examples/event_handling/resample.py b/galleries/examples/event_handling/resample.py index 913cac9cdf0c..f4209ddc6334 100644 --- a/galleries/examples/event_handling/resample.py +++ b/galleries/examples/event_handling/resample.py @@ -22,13 +22,19 @@ # A class that will downsample the data and recompute when zoomed. class DataDisplayDownsampler: - def __init__(self, xdata, ydata): - self.origYData = ydata + def __init__(self, xdata, y1data, y2data): + self.origY1Data = y1data + self.origY2Data = y2data self.origXData = xdata self.max_points = 50 self.delta = xdata[-1] - xdata[0] - def downsample(self, xstart, xend): + def plot(self, ax): + x, y1, y2 = self._downsample(self.origXData.min(), self.origXData.max()) + (self.line,) = ax.plot(x, y1, 'o-') + self.poly_collection = ax.fill_between(x, y1, y2, step="pre", color="r") + + def _downsample(self, xstart, xend): # get the points in the view range mask = (self.origXData > xstart) & (self.origXData < xend) # dilate the mask by one to catch the points just outside @@ -39,36 +45,41 @@ def downsample(self, xstart, xend): # mask data xdata = self.origXData[mask] - ydata = self.origYData[mask] + y1data = self.origY1Data[mask] + y2data = self.origY2Data[mask] # downsample data xdata = xdata[::ratio] - ydata = ydata[::ratio] + y1data = y1data[::ratio] + y2data = y2data[::ratio] - print(f"using {len(ydata)} of {np.sum(mask)} visible points") + print(f"using {len(y1data)} of {np.sum(mask)} visible points") - return xdata, ydata + return xdata, y1data, y2data def update(self, ax): - # Update the line + # Update the artists lims = ax.viewLim if abs(lims.width - self.delta) > 1e-8: self.delta = lims.width xstart, xend = lims.intervalx - self.line.set_data(*self.downsample(xstart, xend)) + x, y1, y2 = self._downsample(xstart, xend) + self.line.set_data(x, y1) + self.poly_collection.set_data(x, y1, y2, step="pre") ax.figure.canvas.draw_idle() # Create a signal xdata = np.linspace(16, 365, (365-16)*4) -ydata = np.sin(2*np.pi*xdata/153) + np.cos(2*np.pi*xdata/127) +y1data = np.sin(2*np.pi*xdata/153) + np.cos(2*np.pi*xdata/127) +y2data = y1data + .2 -d = DataDisplayDownsampler(xdata, ydata) +d = DataDisplayDownsampler(xdata, y1data, y2data) fig, ax = plt.subplots() # Hook up the line -d.line, = ax.plot(xdata, ydata, 'o-') +d.plot(ax) ax.set_autoscale_on(False) # Otherwise, infinite loop # Connect for changing the view limits diff --git a/galleries/examples/event_handling/zoom_window.py b/galleries/examples/event_handling/zoom_window.py index b8ba4c1048a9..6a90a175fb68 100644 --- a/galleries/examples/event_handling/zoom_window.py +++ b/galleries/examples/event_handling/zoom_window.py @@ -1,7 +1,7 @@ """ -=========== -Zoom Window -=========== +======================== +Zoom modifies other Axes +======================== This example shows how to connect events in one window, for example, a mouse press, to another figure window. diff --git a/galleries/examples/images_contours_and_fields/barb_demo.py b/galleries/examples/images_contours_and_fields/barb_demo.py index d3ade99d927c..9229b5262a2c 100644 --- a/galleries/examples/images_contours_and_fields/barb_demo.py +++ b/galleries/examples/images_contours_and_fields/barb_demo.py @@ -1,6 +1,6 @@ """ ========== -Wind Barbs +Wind barbs ========== Demonstration of wind barb plots. diff --git a/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py b/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py index 3ab9074fd1b6..3db799894c95 100644 --- a/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py +++ b/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py @@ -1,6 +1,6 @@ """ ======================================== -Interactive Adjustment of Colormap Range +Interactive adjustment of colormap range ======================================== Demonstration of how a colorbar can be used to interactively adjust the diff --git a/galleries/examples/images_contours_and_fields/contour_corner_mask.py b/galleries/examples/images_contours_and_fields/contour_corner_mask.py index 400f47aa4db5..696231146733 100644 --- a/galleries/examples/images_contours_and_fields/contour_corner_mask.py +++ b/galleries/examples/images_contours_and_fields/contour_corner_mask.py @@ -1,6 +1,6 @@ """ =================== -Contour Corner Mask +Contour corner mask =================== Illustrate the difference between ``corner_mask=False`` and diff --git a/galleries/examples/images_contours_and_fields/contour_image.py b/galleries/examples/images_contours_and_fields/contour_image.py index 3b33233852b7..f60cfee2b61e 100644 --- a/galleries/examples/images_contours_and_fields/contour_image.py +++ b/galleries/examples/images_contours_and_fields/contour_image.py @@ -1,6 +1,6 @@ """ ============= -Contour Image +Contour image ============= Test combinations of contouring, filled contouring, and image plotting. diff --git a/galleries/examples/images_contours_and_fields/contourf_hatching.py b/galleries/examples/images_contours_and_fields/contourf_hatching.py index f8131b41cfa5..020c20b44ec4 100644 --- a/galleries/examples/images_contours_and_fields/contourf_hatching.py +++ b/galleries/examples/images_contours_and_fields/contourf_hatching.py @@ -1,6 +1,6 @@ """ ================= -Contourf Hatching +Contourf hatching ================= Demo filled contour plots with hatched patterns. diff --git a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py index 23d9fd48dff8..2da4e9a1c1cb 100644 --- a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -1,7 +1,7 @@ """ -=========================== -Creating annotated heatmaps -=========================== +================= +Annotated heatmap +================= It is often desirable to show data which depends on two independent variables as a color coded image plot. This is often referred to as a @@ -63,12 +63,9 @@ im = ax.imshow(harvest) # Show all ticks and label them with the respective list entries -ax.set_xticks(np.arange(len(farmers)), labels=farmers) -ax.set_yticks(np.arange(len(vegetables)), labels=vegetables) - -# Rotate the tick labels and set their alignment. -plt.setp(ax.get_xticklabels(), rotation=45, ha="right", - rotation_mode="anchor") +ax.set_xticks(range(len(farmers)), labels=farmers, + rotation=45, ha="right", rotation_mode="anchor") +ax.set_yticks(range(len(vegetables)), labels=vegetables) # Loop over data dimensions and create text annotations. for i in range(len(vegetables)): @@ -137,17 +134,14 @@ def heatmap(data, row_labels, col_labels, ax=None, cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") # Show all ticks and label them with the respective list entries. - ax.set_xticks(np.arange(data.shape[1]), labels=col_labels) - ax.set_yticks(np.arange(data.shape[0]), labels=row_labels) + ax.set_xticks(range(data.shape[1]), labels=col_labels, + rotation=-30, ha="right", rotation_mode="anchor") + ax.set_yticks(range(data.shape[0]), labels=row_labels) # Let the horizontal axes labeling appear on top. ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False) - # Rotate the tick labels and set their alignment. - plt.setp(ax.get_xticklabels(), rotation=-30, ha="right", - rotation_mode="anchor") - # Turn spines off and create white grid. ax.spines[:].set_visible(False) diff --git a/galleries/examples/images_contours_and_fields/image_antialiasing.py b/galleries/examples/images_contours_and_fields/image_antialiasing.py index 18be4f282b67..7f223f6998f2 100644 --- a/galleries/examples/images_contours_and_fields/image_antialiasing.py +++ b/galleries/examples/images_contours_and_fields/image_antialiasing.py @@ -1,34 +1,29 @@ """ -================== -Image antialiasing -================== - -Images are represented by discrete pixels, either on the screen or in an -image file. When data that makes up the image has a different resolution -than its representation on the screen we will see aliasing effects. How -noticeable these are depends on how much down-sampling takes place in -the change of resolution (if any). - -When subsampling data, aliasing is reduced by smoothing first and then -subsampling the smoothed data. In Matplotlib, we can do that -smoothing before mapping the data to colors, or we can do the smoothing -on the RGB(A) data in the final image. The differences between these are -shown below, and controlled with the *interpolation_stage* keyword argument. - -The default image interpolation in Matplotlib is 'antialiased', and -it is applied to the data. This uses a -hanning interpolation on the data provided by the user for reduced aliasing -in most situations. Only when there is upsampling by a factor of 1, 2 or ->=3 is 'nearest' neighbor interpolation used. - -Other anti-aliasing filters can be specified in `.Axes.imshow` using the -*interpolation* keyword argument. +================ +Image resampling +================ + +Images are represented by discrete pixels assigned color values, either on the +screen or in an image file. When a user calls `~.Axes.imshow` with a data +array, it is rare that the size of the data array exactly matches the number of +pixels allotted to the image in the figure, so Matplotlib resamples or `scales +`_ the data or image to fit. If +the data array is larger than the number of pixels allotted in the rendered figure, +then the image will be "down-sampled" and image information will be lost. +Conversely, if the data array is smaller than the number of output pixels then each +data point will get multiple pixels, and the image is "up-sampled". + +In the following figure, the first data array has size (450, 450), but is +represented by far fewer pixels in the figure, and hence is down-sampled. The +second data array has size (4, 4), and is represented by far more pixels, and +hence is up-sampled. """ import matplotlib.pyplot as plt import numpy as np -# %% +fig, axs = plt.subplots(1, 2, figsize=(4, 2)) + # First we generate a 450x450 pixel image with varying frequency content: N = 450 x = np.arange(N) / N - 0.5 @@ -45,71 +40,214 @@ a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1 a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1 aa[:, int(N / 3):] = a[:, int(N / 3):] -a = aa +alarge = aa + +axs[0].imshow(alarge, cmap='RdBu_r') +axs[0].set_title('(450, 450) Down-sampled', fontsize='medium') + +np.random.seed(19680801+9) +asmall = np.random.rand(4, 4) +axs[1].imshow(asmall, cmap='viridis') +axs[1].set_title('(4, 4) Up-sampled', fontsize='medium') + # %% -# The following images are subsampled from 450 data pixels to either -# 125 pixels or 250 pixels (depending on your display). -# The Moiré patterns in the 'nearest' interpolation are caused by the -# high-frequency data being subsampled. The 'antialiased' imaged -# still has some Moiré patterns as well, but they are greatly reduced. +# Matplotlib's `~.Axes.imshow` method has two keyword arguments to allow the user +# to control how resampling is done. The *interpolation* keyword argument allows +# a choice of the kernel that is used for resampling, allowing either `anti-alias +# `_ filtering if +# down-sampling, or smoothing of pixels if up-sampling. The +# *interpolation_stage* keyword argument, determines if this smoothing kernel is +# applied to the underlying data, or if the kernel is applied to the RGBA pixels. # -# There are substantial differences between the 'data' interpolation and -# the 'rgba' interpolation. The alternating bands of red and blue on the -# left third of the image are subsampled. By interpolating in 'data' space -# (the default) the antialiasing filter makes the stripes close to white, -# because the average of -1 and +1 is zero, and zero is white in this -# colormap. +# ``interpolation_stage='rgba'``: Data -> Normalize -> RGBA -> Interpolate/Resample # -# Conversely, when the anti-aliasing occurs in 'rgba' space, the red and -# blue are combined visually to make purple. This behaviour is more like a -# typical image processing package, but note that purple is not in the -# original colormap, so it is no longer possible to invert individual -# pixels back to their data value. - -fig, axs = plt.subplots(2, 2, figsize=(5, 6), layout='constrained') -axs[0, 0].imshow(a, interpolation='nearest', cmap='RdBu_r') -axs[0, 0].set_xlim(100, 200) -axs[0, 0].set_ylim(275, 175) -axs[0, 0].set_title('Zoom') - -for ax, interp, space in zip(axs.flat[1:], - ['nearest', 'antialiased', 'antialiased'], - ['data', 'data', 'rgba']): - ax.imshow(a, interpolation=interp, interpolation_stage=space, +# ``interpolation_stage='data'``: Data -> Interpolate/Resample -> Normalize -> RGBA +# +# For both keyword arguments, Matplotlib has a default "antialiased", that is +# recommended for most situations, and is described below. Note that this +# default behaves differently if the image is being down- or up-sampled, as +# described below. +# +# Down-sampling and modest up-sampling +# ==================================== +# +# When down-sampling data, we usually want to remove aliasing by smoothing the +# image first and then sub-sampling it. In Matplotlib, we can do that smoothing +# before mapping the data to colors, or we can do the smoothing on the RGB(A) +# image pixels. The differences between these are shown below, and controlled +# with the *interpolation_stage* keyword argument. +# +# The following images are down-sampled from 450 data pixels to approximately +# 125 pixels or 250 pixels (depending on your display). +# The underlying image has alternating +1, -1 stripes on the left side, and +# a varying wavelength (`chirp `_) pattern +# in the rest of the image. If we zoom, we can see this detail without any +# down-sampling: + +fig, ax = plt.subplots(figsize=(4, 4), layout='compressed') +ax.imshow(alarge, interpolation='nearest', cmap='RdBu_r') +ax.set_xlim(100, 200) +ax.set_ylim(275, 175) +ax.set_title('Zoom') + +# %% +# If we down-sample, the simplest algorithm is to decimate the data using +# `nearest-neighbor interpolation +# `_. We can +# do this in either data space or RGBA space: + +fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed') +for ax, interp, space in zip(axs.flat, ['nearest', 'nearest'], + ['data', 'rgba']): + ax.imshow(alarge, interpolation=interp, interpolation_stage=space, cmap='RdBu_r') - ax.set_title(f"interpolation='{interp}'\nspace='{space}'") + ax.set_title(f"interpolation='{interp}'\nstage='{space}'") + +# %% +# Nearest interpolation is identical in data and RGBA space, and both exhibit +# `Moiré `_ patterns because the +# high-frequency data is being down-sampled and shows up as lower frequency +# patterns. We can reduce the Moiré patterns by applying an anti-aliasing filter +# to the image before rendering: + +fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed') +for ax, interp, space in zip(axs.flat, ['hanning', 'hanning'], + ['data', 'rgba']): + ax.imshow(alarge, interpolation=interp, interpolation_stage=space, + cmap='RdBu_r') + ax.set_title(f"interpolation='{interp}'\nstage='{space}'") plt.show() # %% -# Even up-sampling an image with 'nearest' interpolation will lead to Moiré -# patterns when the upsampling factor is not integer. The following image -# upsamples 500 data pixels to 530 rendered pixels. You may note a grid of -# 30 line-like artifacts which stem from the 524 - 500 = 24 extra pixels that -# had to be made up. Since interpolation is 'nearest' they are the same as a -# neighboring line of pixels and thus stretch the image locally so that it -# looks distorted. +# The `Hanning `_ filter smooths +# the underlying data so that each new pixel is a weighted average of the +# original underlying pixels. This greatly reduces the Moiré patterns. +# However, when the *interpolation_stage* is set to 'data', it also introduces +# white regions to the image that are not in the original data, both in the +# alternating bands on the left hand side of the image, and in the boundary +# between the red and blue of the large circles in the middle of the image. +# The interpolation at the 'rgba' stage has a different artifact, with the alternating +# bands coming out a shade of purple; even though purple is not in the original +# colormap, it is what we perceive when a blue and red stripe are close to each +# other. +# +# The default for the *interpolation* keyword argument is 'auto' which +# will choose a Hanning filter if the image is being down-sampled or up-sampled +# by less than a factor of three. The default *interpolation_stage* keyword +# argument is also 'auto', and for images that are down-sampled or +# up-sampled by less than a factor of three it defaults to 'rgba' +# interpolation. +# +# Anti-aliasing filtering is needed, even when up-sampling. The following image +# up-samples 450 data pixels to 530 rendered pixels. You may note a grid of +# line-like artifacts which stem from the extra pixels that had to be made up. +# Since interpolation is 'nearest' they are the same as a neighboring line of +# pixels and thus stretch the image locally so that it looks distorted. + fig, ax = plt.subplots(figsize=(6.8, 6.8)) -ax.imshow(a, interpolation='nearest', cmap='gray') -ax.set_title("upsampled by factor a 1.048, interpolation='nearest'") -plt.show() +ax.imshow(alarge, interpolation='nearest', cmap='grey') +ax.set_title("up-sampled by factor a 1.17, interpolation='nearest'") # %% -# Better antialiasing algorithms can reduce this effect: +# Better anti-aliasing algorithms can reduce this effect: fig, ax = plt.subplots(figsize=(6.8, 6.8)) -ax.imshow(a, interpolation='antialiased', cmap='gray') -ax.set_title("upsampled by factor a 1.048, interpolation='antialiased'") -plt.show() +ax.imshow(alarge, interpolation='auto', cmap='grey') +ax.set_title("up-sampled by factor a 1.17, interpolation='auto'") # %% -# Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a +# Apart from the default 'hanning' anti-aliasing, `~.Axes.imshow` supports a # number of different interpolation algorithms, which may work better or -# worse depending on the pattern. +# worse depending on the underlying data. fig, axs = plt.subplots(1, 2, figsize=(7, 4), layout='constrained') for ax, interp in zip(axs, ['hanning', 'lanczos']): - ax.imshow(a, interpolation=interp, cmap='gray') + ax.imshow(alarge, interpolation=interp, cmap='gray') ax.set_title(f"interpolation='{interp}'") + +# %% +# A final example shows the desirability of performing the anti-aliasing at the +# RGBA stage when using non-trivial interpolation kernels. In the following, +# the data in the upper 100 rows is exactly 0.0, and data in the inner circle +# is exactly 2.0. If we perform the *interpolation_stage* in 'data' space and +# use an anti-aliasing filter (first panel), then floating point imprecision +# makes some of the data values just a bit less than zero or a bit more than +# 2.0, and they get assigned the under- or over- colors. This can be avoided if +# you do not use an anti-aliasing filter (*interpolation* set set to +# 'nearest'), however, that makes the part of the data susceptible to Moiré +# patterns much worse (second panel). Therefore, we recommend the default +# *interpolation* of 'hanning'/'auto', and *interpolation_stage* of +# 'rgba'/'auto' for most down-sampling situations (last panel). + +a = alarge + 1 +cmap = plt.get_cmap('RdBu_r') +cmap.set_under('yellow') +cmap.set_over('limegreen') + +fig, axs = plt.subplots(1, 3, figsize=(7, 3), layout='constrained') +for ax, interp, space in zip(axs.flat, + ['hanning', 'nearest', 'hanning', ], + ['data', 'data', 'rgba']): + im = ax.imshow(a, interpolation=interp, interpolation_stage=space, + cmap=cmap, vmin=0, vmax=2) + title = f"interpolation='{interp}'\nstage='{space}'" + if ax == axs[2]: + title += '\nDefault' + ax.set_title(title, fontsize='medium') +fig.colorbar(im, ax=axs, extend='both', shrink=0.8) + +# %% +# Up-sampling +# =========== +# +# If we up-sample, then we can represent a data pixel by many image or screen pixels. +# In the following example, we greatly over-sample the small data matrix. + +np.random.seed(19680801+9) +a = np.random.rand(4, 4) + +fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed') +axs[0].imshow(asmall, cmap='viridis') +axs[0].set_title("interpolation='auto'\nstage='auto'") +axs[1].imshow(asmall, cmap='viridis', interpolation="nearest", + interpolation_stage="data") +axs[1].set_title("interpolation='nearest'\nstage='data'") plt.show() +# %% +# The *interpolation* keyword argument can be used to smooth the pixels if desired. +# However, that almost always is better done in data space, rather than in RGBA space +# where the filters can cause colors that are not in the colormap to be the result of +# the interpolation. In the following example, note that when the interpolation is +# 'rgba' there are red colors as interpolation artifacts. Therefore, the default +# 'auto' choice for *interpolation_stage* is set to be the same as 'data' +# when up-sampling is greater than a factor of three: + +fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed') +im = axs[0].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='data') +axs[0].set_title("interpolation='sinc'\nstage='data'\n(default for upsampling)") +axs[1].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='rgba') +axs[1].set_title("interpolation='sinc'\nstage='rgba'") +fig.colorbar(im, ax=axs, shrink=0.7, extend='both') + +# %% +# Avoiding resampling +# =================== +# +# It is possible to avoid resampling data when making an image. One method is +# to simply save to a vector backend (pdf, eps, svg) and use +# ``interpolation='none'``. Vector backends allow embedded images, however be +# aware that some vector image viewers may smooth image pixels. +# +# The second method is to exactly match the size of your axes to the size of +# your data. The following figure is exactly 2 inches by 2 inches, and +# if the dpi is 200, then the 400x400 data is not resampled at all. If you download +# this image and zoom in an image viewer you should see the individual stripes +# on the left hand side (note that if you have a non hiDPI or "retina" screen, the html +# may serve a 100x100 version of the image, which will be downsampled.) + +fig = plt.figure(figsize=(2, 2)) +ax = fig.add_axes([0, 0, 1, 1]) +ax.imshow(aa[:400, :400], cmap='RdBu_r', interpolation='nearest') +plt.show() # %% # # .. admonition:: References diff --git a/galleries/examples/images_contours_and_fields/image_masked.py b/galleries/examples/images_contours_and_fields/image_masked.py index d64ab2cff8c7..3d4058c62eb7 100644 --- a/galleries/examples/images_contours_and_fields/image_masked.py +++ b/galleries/examples/images_contours_and_fields/image_masked.py @@ -1,7 +1,7 @@ """ -============ -Image Masked -============ +======================== +Image with masked values +======================== imshow with masked array input and out-of-range colors. diff --git a/galleries/examples/images_contours_and_fields/interpolation_methods.py b/galleries/examples/images_contours_and_fields/interpolation_methods.py index 496b39c56b9f..dea1b474801c 100644 --- a/galleries/examples/images_contours_and_fields/interpolation_methods.py +++ b/galleries/examples/images_contours_and_fields/interpolation_methods.py @@ -8,14 +8,14 @@ If *interpolation* is None, it defaults to the :rc:`image.interpolation`. If the interpolation is ``'none'``, then no interpolation is performed for the -Agg, ps and pdf backends. Other backends will default to ``'antialiased'``. +Agg, ps and pdf backends. Other backends will default to ``'auto'``. For the Agg, ps and pdf backends, ``interpolation='none'`` works well when a big image is scaled down, while ``interpolation='nearest'`` works well when a small image is scaled up. See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a -discussion on the default ``interpolation='antialiased'`` option. +discussion on the default ``interpolation='auto'`` option. """ import matplotlib.pyplot as plt diff --git a/galleries/examples/images_contours_and_fields/layer_images.py b/galleries/examples/images_contours_and_fields/layer_images.py index bcaa25471500..c67c08960ecd 100644 --- a/galleries/examples/images_contours_and_fields/layer_images.py +++ b/galleries/examples/images_contours_and_fields/layer_images.py @@ -1,7 +1,7 @@ """ -============ -Layer Images -============ +================================ +Layer images with alpha blending +================================ Layer images above one another using alpha blending """ diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 8be048055dec..4e6f6cc54a79 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -63,8 +63,8 @@ # # def sync_cmaps(changed_image): # for im in images: -# if changed_image.get_cmap() != im.get_cmap(): -# im.set_cmap(changed_image.get_cmap()) +# if changed_image.get_cmap() != im.get_cmap(): +# im.set_cmap(changed_image.get_cmap()) # # for im in images: # im.callbacks.connect('changed', sync_cmaps) diff --git a/galleries/examples/lines_bars_and_markers/bar_colors.py b/galleries/examples/lines_bars_and_markers/bar_colors.py index 35e7a64ef605..1692c222957d 100644 --- a/galleries/examples/lines_bars_and_markers/bar_colors.py +++ b/galleries/examples/lines_bars_and_markers/bar_colors.py @@ -1,7 +1,7 @@ """ -============== -Bar color demo -============== +==================================== +Bar chart with individual bar colors +==================================== This is an example showing how to control bar color and legend entries using the *color* and *label* parameters of `~matplotlib.pyplot.bar`. @@ -24,3 +24,10 @@ ax.legend(title='Fruit color') plt.show() + +# %% +# .. tags:: +# +# styling: color +# plot-style: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/bar_label_demo.py b/galleries/examples/lines_bars_and_markers/bar_label_demo.py index d60bd2a16299..2e43dbb18013 100644 --- a/galleries/examples/lines_bars_and_markers/bar_label_demo.py +++ b/galleries/examples/lines_bars_and_markers/bar_label_demo.py @@ -1,7 +1,7 @@ """ -============== -Bar Label Demo -============== +===================== +Bar chart with labels +===================== This example shows how to use the `~.Axes.bar_label` helper function to create bar chart labels. @@ -118,3 +118,9 @@ # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.barh` / `matplotlib.pyplot.barh` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` +# +# .. tags:: +# +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/bar_stacked.py b/galleries/examples/lines_bars_and_markers/bar_stacked.py index 81ee305e7072..f1f97e89da13 100644 --- a/galleries/examples/lines_bars_and_markers/bar_stacked.py +++ b/galleries/examples/lines_bars_and_markers/bar_stacked.py @@ -34,3 +34,9 @@ ax.legend(loc="upper right") plt.show() + +# %% +# .. tags:: +# +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/barchart.py b/galleries/examples/lines_bars_and_markers/barchart.py index c533ca2eda37..f2157a89c0cd 100644 --- a/galleries/examples/lines_bars_and_markers/barchart.py +++ b/galleries/examples/lines_bars_and_markers/barchart.py @@ -49,3 +49,9 @@ # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` +# +# .. tags:: +# +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 4de8bc85d3d5..5493c7456c75 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -26,3 +26,9 @@ ax.set_title('How fast do you want to go today?') plt.show() + +# %% +# .. tags:: +# +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/broken_barh.py b/galleries/examples/lines_bars_and_markers/broken_barh.py index 6da38f1e465f..e1550385155a 100644 --- a/galleries/examples/lines_bars_and_markers/broken_barh.py +++ b/galleries/examples/lines_bars_and_markers/broken_barh.py @@ -24,3 +24,10 @@ horizontalalignment='right', verticalalignment='top') plt.show() + +# %% +# .. tags:: +# +# component: annotation +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/capstyle.py b/galleries/examples/lines_bars_and_markers/capstyle.py index d17f86c6be58..4927c904caa7 100644 --- a/galleries/examples/lines_bars_and_markers/capstyle.py +++ b/galleries/examples/lines_bars_and_markers/capstyle.py @@ -14,3 +14,8 @@ CapStyle.demo() plt.show() + +# %% +# .. tags:: +# +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/categorical_variables.py b/galleries/examples/lines_bars_and_markers/categorical_variables.py index e28dda0dda47..4cceb38fbd4d 100644 --- a/galleries/examples/lines_bars_and_markers/categorical_variables.py +++ b/galleries/examples/lines_bars_and_markers/categorical_variables.py @@ -32,3 +32,9 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# +# plot-type: specialty +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/cohere.py b/galleries/examples/lines_bars_and_markers/cohere.py index 64124e37645e..f02788ea1d69 100644 --- a/galleries/examples/lines_bars_and_markers/cohere.py +++ b/galleries/examples/lines_bars_and_markers/cohere.py @@ -27,7 +27,14 @@ axs[0].set_ylabel('s1 and s2') axs[0].grid(True) -cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) +cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt) axs[1].set_ylabel('Coherence') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/csd_demo.py b/galleries/examples/lines_bars_and_markers/csd_demo.py index b2d903ae0885..76d9f0825223 100644 --- a/galleries/examples/lines_bars_and_markers/csd_demo.py +++ b/galleries/examples/lines_bars_and_markers/csd_demo.py @@ -34,7 +34,14 @@ ax1.set_ylabel('s1 and s2') ax1.grid(True) -cxy, f = ax2.csd(s1, s2, 256, 1. / dt) +cxy, f = ax2.csd(s1, s2, NFFT=256, Fs=1. / dt) ax2.set_ylabel('CSD (dB)') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/curve_error_band.py b/galleries/examples/lines_bars_and_markers/curve_error_band.py index 61c73e415163..320d2e710be6 100644 --- a/galleries/examples/lines_bars_and_markers/curve_error_band.py +++ b/galleries/examples/lines_bars_and_markers/curve_error_band.py @@ -85,3 +85,9 @@ def draw_error_band(ax, x, y, err, **kwargs): # # - `matplotlib.patches.PathPatch` # - `matplotlib.path.Path` +# +# .. tags:: +# +# component: error +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py b/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py index aff01eece49a..d9c8375c61fb 100644 --- a/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py +++ b/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py @@ -60,3 +60,9 @@ # in this example: # # - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar` +# +# .. tags:: +# +# component: error +# plot-type: errorbar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/errorbar_subsample.py b/galleries/examples/lines_bars_and_markers/errorbar_subsample.py index e5aa84577231..009286e28ea9 100644 --- a/galleries/examples/lines_bars_and_markers/errorbar_subsample.py +++ b/galleries/examples/lines_bars_and_markers/errorbar_subsample.py @@ -38,3 +38,10 @@ fig.suptitle('Errorbar subsampling') plt.show() + +# %% +# .. tags:: +# +# component: error +# plot-type: errorbar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/eventcollection_demo.py b/galleries/examples/lines_bars_and_markers/eventcollection_demo.py index 18783e1649bc..1aa2fa622812 100644 --- a/galleries/examples/lines_bars_and_markers/eventcollection_demo.py +++ b/galleries/examples/lines_bars_and_markers/eventcollection_demo.py @@ -60,3 +60,9 @@ # display the plot plt.show() + +# %% +# .. tags:: +# +# plot-type: eventplot +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/eventplot_demo.py b/galleries/examples/lines_bars_and_markers/eventplot_demo.py index b76999ef05d5..17797c2f697a 100644 --- a/galleries/examples/lines_bars_and_markers/eventplot_demo.py +++ b/galleries/examples/lines_bars_and_markers/eventplot_demo.py @@ -60,3 +60,10 @@ linelengths=linelengths2, orientation='vertical') plt.show() + +# %% +# .. tags:: +# +# plot-type: eventplot +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill.py b/galleries/examples/lines_bars_and_markers/fill.py index a9cba03c273c..4eba083fa825 100644 --- a/galleries/examples/lines_bars_and_markers/fill.py +++ b/galleries/examples/lines_bars_and_markers/fill.py @@ -85,3 +85,9 @@ def _koch_snowflake_complex(order): # # - `matplotlib.axes.Axes.fill` / `matplotlib.pyplot.fill` # - `matplotlib.axes.Axes.axis` / `matplotlib.pyplot.axis` +# +# .. tags:: +# +# styling: shape +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py index 3894d9d1d45c..f462f6bf2428 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py @@ -1,6 +1,7 @@ """ -Fill Between and Alpha -====================== +================================== +``fill_between`` with transparency +================================== The `~matplotlib.axes.Axes.fill_between` function generates a shaded region between a min and max boundary that is useful for illustrating ranges. @@ -136,3 +137,11 @@ # :doc:`/gallery/subplots_axes_and_figures/axhspan_demo`. plt.show() + +# %% +# .. tags:: +# +# styling: alpha +# plot-type: fill_between +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_between_demo.py b/galleries/examples/lines_bars_and_markers/fill_between_demo.py index 656a8695ba18..feb325a3f9db 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_demo.py @@ -1,7 +1,7 @@ """ -============================== -Filling the area between lines -============================== +=============================== +Fill the area between two lines +=============================== This example shows how to use `~.axes.Axes.fill_between` to color the area between two lines. @@ -139,3 +139,10 @@ # # - `matplotlib.axes.Axes.fill_between` / `matplotlib.pyplot.fill_between` # - `matplotlib.axes.Axes.get_xaxis_transform` +# +# .. tags:: +# +# styling: conditional +# plot-type: fill_between +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py index b311db42af85..ebd8d2a24a7b 100644 --- a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py @@ -1,7 +1,7 @@ """ -================== -Fill Betweenx Demo -================== +======================================== +Fill the area between two vertical lines +======================================== Using `~.Axes.fill_betweenx` to color along the horizontal direction between two curves. @@ -52,3 +52,9 @@ # would be to interpolate all arrays to a very fine grid before plotting. plt.show() + +# %% +# .. tags:: +# +# plot-type: fill_between +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/filled_step.py b/galleries/examples/lines_bars_and_markers/filled_step.py deleted file mode 100644 index 65a7d31a425a..000000000000 --- a/galleries/examples/lines_bars_and_markers/filled_step.py +++ /dev/null @@ -1,237 +0,0 @@ -""" -========================= -Hatch-filled histograms -========================= - -Hatching capabilities for plotting histograms. -""" - -from functools import partial -import itertools - -from cycler import cycler - -import matplotlib.pyplot as plt -import numpy as np - -import matplotlib.ticker as mticker - - -def filled_hist(ax, edges, values, bottoms=None, orientation='v', - **kwargs): - """ - Draw a histogram as a stepped patch. - - Parameters - ---------- - ax : Axes - The Axes to plot to. - - edges : array - A length n+1 array giving the left edges of each bin and the - right edge of the last bin. - - values : array - A length n array of bin counts or values - - bottoms : float or array, optional - A length n array of the bottom of the bars. If None, zero is used. - - orientation : {'v', 'h'} - Orientation of the histogram. 'v' (default) has - the bars increasing in the positive y-direction. - - **kwargs - Extra keyword arguments are passed through to `.fill_between`. - - Returns - ------- - ret : PolyCollection - Artist added to the Axes - """ - print(orientation) - if orientation not in 'hv': - raise ValueError(f"orientation must be in {{'h', 'v'}} " - f"not {orientation}") - - kwargs.setdefault('step', 'post') - kwargs.setdefault('alpha', 0.7) - edges = np.asarray(edges) - values = np.asarray(values) - if len(edges) - 1 != len(values): - raise ValueError(f'Must provide one more bin edge than value not: ' - f'{len(edges)=} {len(values)=}') - - if bottoms is None: - bottoms = 0 - bottoms = np.broadcast_to(bottoms, values.shape) - - values = np.append(values, values[-1]) - bottoms = np.append(bottoms, bottoms[-1]) - if orientation == 'h': - return ax.fill_betweenx(edges, values, bottoms, - **kwargs) - elif orientation == 'v': - return ax.fill_between(edges, values, bottoms, - **kwargs) - else: - raise AssertionError("you should never be here") - - -def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, - hist_func=None, labels=None, - plot_func=None, plot_kwargs=None): - """ - Parameters - ---------- - ax : axes.Axes - The Axes to add artists to. - - stacked_data : array or Mapping - A (M, N) shaped array. The first dimension will be iterated over to - compute histograms row-wise - - sty_cycle : Cycler or operable of dict - Style to apply to each set - - bottoms : array, default: 0 - The initial positions of the bottoms. - - hist_func : callable, optional - Must have signature `bin_vals, bin_edges = f(data)`. - `bin_edges` expected to be one longer than `bin_vals` - - labels : list of str, optional - The label for each set. - - If not given and stacked data is an array defaults to 'default set {n}' - - If *stacked_data* is a mapping, and *labels* is None, default to the - keys. - - If *stacked_data* is a mapping and *labels* is given then only the - columns listed will be plotted. - - plot_func : callable, optional - Function to call to draw the histogram must have signature: - - ret = plot_func(ax, edges, top, bottoms=bottoms, - label=label, **kwargs) - - plot_kwargs : dict, optional - Any extra keyword arguments to pass through to the plotting function. - This will be the same for all calls to the plotting function and will - override the values in *sty_cycle*. - - Returns - ------- - arts : dict - Dictionary of artists keyed on their labels - """ - # deal with default binning function - if hist_func is None: - hist_func = np.histogram - - # deal with default plotting function - if plot_func is None: - plot_func = filled_hist - - # deal with default - if plot_kwargs is None: - plot_kwargs = {} - print(plot_kwargs) - try: - l_keys = stacked_data.keys() - label_data = True - if labels is None: - labels = l_keys - - except AttributeError: - label_data = False - if labels is None: - labels = itertools.repeat(None) - - if label_data: - loop_iter = enumerate((stacked_data[lab], lab, s) - for lab, s in zip(labels, sty_cycle)) - else: - loop_iter = enumerate(zip(stacked_data, labels, sty_cycle)) - - arts = {} - for j, (data, label, sty) in loop_iter: - if label is None: - label = f'dflt set {j}' - label = sty.pop('label', label) - vals, edges = hist_func(data) - if bottoms is None: - bottoms = np.zeros_like(vals) - top = bottoms + vals - print(sty) - sty.update(plot_kwargs) - print(sty) - ret = plot_func(ax, edges, top, bottoms=bottoms, - label=label, **sty) - bottoms = top - arts[label] = ret - ax.legend(fontsize=10) - return arts - - -# set up histogram function to fixed bins -edges = np.linspace(-3, 3, 20, endpoint=True) -hist_func = partial(np.histogram, bins=edges) - -# set up style cycles -color_cycle = cycler(facecolor=plt.rcParams['axes.prop_cycle'][:4]) -label_cycle = cycler(label=[f'set {n}' for n in range(4)]) -hatch_cycle = cycler(hatch=['/', '*', '+', '|']) - -# Fixing random state for reproducibility -np.random.seed(19680801) - -stack_data = np.random.randn(4, 12250) -dict_data = dict(zip((c['label'] for c in label_cycle), stack_data)) - -# %% -# Work with plain arrays - -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), tight_layout=True) -arts = stack_hist(ax1, stack_data, color_cycle + label_cycle + hatch_cycle, - hist_func=hist_func) - -arts = stack_hist(ax2, stack_data, color_cycle, - hist_func=hist_func, - plot_kwargs=dict(edgecolor='w', orientation='h')) -ax1.set_ylabel('counts') -ax1.set_xlabel('x') -ax2.set_xlabel('counts') -ax2.set_ylabel('x') - -# %% -# Work with labeled data - -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), - tight_layout=True, sharey=True) - -arts = stack_hist(ax1, dict_data, color_cycle + hatch_cycle, - hist_func=hist_func) - -arts = stack_hist(ax2, dict_data, color_cycle + hatch_cycle, - hist_func=hist_func, labels=['set 0', 'set 3']) -ax1.xaxis.set_major_locator(mticker.MaxNLocator(5)) -ax1.set_xlabel('counts') -ax1.set_ylabel('x') -ax2.set_ylabel('x') - -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.fill_betweenx` / `matplotlib.pyplot.fill_betweenx` -# - `matplotlib.axes.Axes.fill_between` / `matplotlib.pyplot.fill_between` -# - `matplotlib.axis.Axis.set_major_locator` diff --git a/galleries/examples/lines_bars_and_markers/gradient_bar.py b/galleries/examples/lines_bars_and_markers/gradient_bar.py index 4cd86f26590f..2e9e2c8aa4aa 100644 --- a/galleries/examples/lines_bars_and_markers/gradient_bar.py +++ b/galleries/examples/lines_bars_and_markers/gradient_bar.py @@ -71,3 +71,11 @@ def gradient_bar(ax, x, y, width=0.5, bottom=0): y = np.random.rand(N) gradient_bar(ax, x, y, width=0.7) plt.show() + +# %% +# .. tags:: +# +# styling: color +# plot-type: imshow +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/hat_graph.py b/galleries/examples/lines_bars_and_markers/hat_graph.py index 0091c3c95d51..0f6d934f1cb1 100644 --- a/galleries/examples/lines_bars_and_markers/hat_graph.py +++ b/galleries/examples/lines_bars_and_markers/hat_graph.py @@ -78,3 +78,9 @@ def label_bars(heights, rects): # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` +# +# .. tags:: +# +# component: annotate +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py b/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py index ae638a90c3fd..3f7e499f38e7 100644 --- a/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py +++ b/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py @@ -78,3 +78,10 @@ def survey(results, category_names): # - `matplotlib.axes.Axes.barh` / `matplotlib.pyplot.barh` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` +# +# .. tags:: +# +# domain: statistics +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/joinstyle.py b/galleries/examples/lines_bars_and_markers/joinstyle.py index 27ad9ede96b1..09ae03e07692 100644 --- a/galleries/examples/lines_bars_and_markers/joinstyle.py +++ b/galleries/examples/lines_bars_and_markers/joinstyle.py @@ -14,3 +14,6 @@ JoinStyle.demo() plt.show() + +# %% +# .. tags:: purpose: reference, styling: linestyle diff --git a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py index c695bc51c176..3b3880794d3d 100644 --- a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py +++ b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py @@ -1,7 +1,7 @@ """ -============================== -Customizing dashed line styles -============================== +=============================== +Dashed line style configuration +=============================== The dashing of a line is controlled via a dash sequence. It can be modified using `.Line2D.set_dashes`. @@ -47,3 +47,10 @@ ax.legend(handlelength=4) plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# plot-style: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py b/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py index 00776d89caff..fba7eb9f045e 100644 --- a/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py +++ b/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py @@ -30,3 +30,10 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/linestyles.py b/galleries/examples/lines_bars_and_markers/linestyles.py index f3ce3aa6e7ec..25b053e912bd 100644 --- a/galleries/examples/lines_bars_and_markers/linestyles.py +++ b/galleries/examples/lines_bars_and_markers/linestyles.py @@ -8,7 +8,12 @@ ``(offset, (on_off_seq))``. For example, ``(0, (3, 10, 1, 15))`` means (3pt line, 10pt space, 1pt line, 15pt space) with no offset, while ``(5, (10, 3))``, means (10pt line, 3pt space), but skip the first 5pt line. -See also `.Line2D.set_linestyle`. +See also `.Line2D.set_linestyle`. The specific on/off sequences of the +"dotted", "dashed" and "dashdot" styles are configurable: + +* :rc:`lines.dotted_pattern` +* :rc:`lines.dashed_pattern` +* :rc:`lines.dashdot_pattern` *Note*: The dash style can also be configured via `.Line2D.set_dashes` as shown in :doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` @@ -20,14 +25,15 @@ linestyle_str = [ ('solid', 'solid'), # Same as (0, ()) or '-' - ('dotted', 'dotted'), # Same as (0, (1, 1)) or ':' + ('dotted', 'dotted'), # Same as ':' ('dashed', 'dashed'), # Same as '--' ('dashdot', 'dashdot')] # Same as '-.' linestyle_tuple = [ ('loosely dotted', (0, (1, 10))), - ('dotted', (0, (1, 1))), + ('dotted', (0, (1, 5))), ('densely dotted', (0, (1, 1))), + ('long dash with offset', (5, (10, 3))), ('loosely dashed', (0, (5, 10))), ('dashed', (0, (5, 5))), @@ -66,10 +72,16 @@ def plot_linestyles(ax, linestyles, title): color="blue", fontsize=8, ha="right", family="monospace") -fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(10, 8), height_ratios=[1, 3]) +fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(7, 8), height_ratios=[1, 3], + layout='constrained') plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') plot_linestyles(ax1, linestyle_tuple[::-1], title='Parametrized linestyles') -plt.tight_layout() plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/marker_reference.py b/galleries/examples/lines_bars_and_markers/marker_reference.py index f99afb08e143..32b6291cbc76 100644 --- a/galleries/examples/lines_bars_and_markers/marker_reference.py +++ b/galleries/examples/lines_bars_and_markers/marker_reference.py @@ -240,3 +240,9 @@ def split_list(a_list): format_axes(ax) plt.show() + +# %% +# .. tags:: +# +# component: marker +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/markevery_demo.py b/galleries/examples/lines_bars_and_markers/markevery_demo.py index cc02fb5ee576..919e12cde952 100644 --- a/galleries/examples/lines_bars_and_markers/markevery_demo.py +++ b/galleries/examples/lines_bars_and_markers/markevery_demo.py @@ -96,3 +96,10 @@ ax.plot(theta, r, 'o', ls='-', ms=4, markevery=markevery) plt.show() + +# %% +# .. tags:: +# +# component: marker +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/masked_demo.py b/galleries/examples/lines_bars_and_markers/masked_demo.py index a0b63ae30fe4..842c5c022f0b 100644 --- a/galleries/examples/lines_bars_and_markers/masked_demo.py +++ b/galleries/examples/lines_bars_and_markers/masked_demo.py @@ -48,3 +48,9 @@ plt.legend() plt.title('Masked and NaN data') plt.show() + +# %% +# .. tags:: +# +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/multicolored_line.py b/galleries/examples/lines_bars_and_markers/multicolored_line.py index 3d14ecaf8567..8c72d28e9e67 100644 --- a/galleries/examples/lines_bars_and_markers/multicolored_line.py +++ b/galleries/examples/lines_bars_and_markers/multicolored_line.py @@ -188,3 +188,11 @@ def colored_line_between_pts(x, y, c, ax, **lc_kwargs): ax2.set_title("Color between points") plt.show() + +# %% +# .. tags:: +# +# styling: color +# styling: linestyle +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py index 13609422e690..d05085a0d9dc 100644 --- a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -45,3 +45,12 @@ ax.set_ylabel("Y position [m]") plt.show() + +# %% +# .. tags:: +# +# component: marker +# styling: color, +# styling: shape +# level: beginner +# purpose: fun diff --git a/galleries/examples/lines_bars_and_markers/psd_demo.py b/galleries/examples/lines_bars_and_markers/psd_demo.py index 52587fd6d7bf..edbfc79289af 100644 --- a/galleries/examples/lines_bars_and_markers/psd_demo.py +++ b/galleries/examples/lines_bars_and_markers/psd_demo.py @@ -30,7 +30,7 @@ ax0.plot(t, s) ax0.set_xlabel('Time (s)') ax0.set_ylabel('Signal') -ax1.psd(s, 512, 1 / dt) +ax1.psd(s, NFFT=512, Fs=1 / dt) plt.show() @@ -178,3 +178,10 @@ ax1.set_ylim(yrange) plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/scatter_demo2.py b/galleries/examples/lines_bars_and_markers/scatter_demo2.py index c3d57c423d69..33e532bb9af9 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_demo2.py +++ b/galleries/examples/lines_bars_and_markers/scatter_demo2.py @@ -34,3 +34,11 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: marker +# component: color +# plot-style: scatter +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_hist.py b/galleries/examples/lines_bars_and_markers/scatter_hist.py index 95a373961aa1..505edf6d627f 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_hist.py +++ b/galleries/examples/lines_bars_and_markers/scatter_hist.py @@ -120,3 +120,10 @@ def scatter_hist(x, y, ax, ax_histx, ax_histy): # - `matplotlib.axes.Axes.inset_axes` # - `matplotlib.axes.Axes.scatter` # - `matplotlib.axes.Axes.hist` +# +# .. tags:: +# +# component: axes +# plot-type: scatter +# plot-type: histogram +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/scatter_masked.py b/galleries/examples/lines_bars_and_markers/scatter_masked.py index 22c0943bf28a..97132e85192e 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_masked.py +++ b/galleries/examples/lines_bars_and_markers/scatter_masked.py @@ -1,7 +1,7 @@ """ -============== -Scatter Masked -============== +=============================== +Scatter plot with masked values +=============================== Mask some data points and add a line demarking masked regions. @@ -30,3 +30,10 @@ plt.plot(r0 * np.cos(theta), r0 * np.sin(theta)) plt.show() + +# %% +# .. tags:: +# +# component: marker +# plot-type: scatter +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_star_poly.py b/galleries/examples/lines_bars_and_markers/scatter_star_poly.py index d97408333455..c2fee968ad7b 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_star_poly.py +++ b/galleries/examples/lines_bars_and_markers/scatter_star_poly.py @@ -51,3 +51,9 @@ axs[1, 2].set_title("marker=(5, 2)") plt.show() + +# %% +# .. tags:: +# +# component: marker +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py index 786ffff18807..e9f19981fe4a 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py +++ b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py @@ -1,7 +1,7 @@ """ -=========================== -Scatter plots with a legend -=========================== +========================== +Scatter plot with a legend +========================== To create a scatter plot with a legend one may use a loop and create one `~.Axes.scatter` plot per item to appear in the legend and set the ``label`` @@ -109,3 +109,9 @@ # - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # - `matplotlib.collections.PathCollection.legend_elements` +# +# .. tags:: +# +# component: legend +# plot-type: scatter +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/simple_plot.py b/galleries/examples/lines_bars_and_markers/simple_plot.py index 520d6fac8204..bcac888b8c4a 100644 --- a/galleries/examples/lines_bars_and_markers/simple_plot.py +++ b/galleries/examples/lines_bars_and_markers/simple_plot.py @@ -1,9 +1,9 @@ """ -=========== -Simple Plot -=========== +========= +Line plot +========= -Create a simple plot. +Create a basic line plot. """ import matplotlib.pyplot as plt @@ -33,3 +33,8 @@ # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` # - `matplotlib.pyplot.subplots` # - `matplotlib.figure.Figure.savefig` +# +# .. tags:: +# +# plot-style: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/span_regions.py b/galleries/examples/lines_bars_and_markers/span_regions.py index e73b1af47baa..8128bd3b865b 100644 --- a/galleries/examples/lines_bars_and_markers/span_regions.py +++ b/galleries/examples/lines_bars_and_markers/span_regions.py @@ -29,3 +29,9 @@ # in this example: # # - `matplotlib.axes.Axes.fill_between` +# +# .. tags:: +# +# styling: conditional +# plot-style: fill_between +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/spectrum_demo.py b/galleries/examples/lines_bars_and_markers/spectrum_demo.py index 147d802b6eff..57706e22be9d 100644 --- a/galleries/examples/lines_bars_and_markers/spectrum_demo.py +++ b/galleries/examples/lines_bars_and_markers/spectrum_demo.py @@ -49,3 +49,10 @@ axs["angle"].angle_spectrum(s, Fs=Fs, color='C2') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/stackplot_demo.py b/galleries/examples/lines_bars_and_markers/stackplot_demo.py index d02a9af73da3..2ed52ed9a2ce 100644 --- a/galleries/examples/lines_bars_and_markers/stackplot_demo.py +++ b/galleries/examples/lines_bars_and_markers/stackplot_demo.py @@ -73,3 +73,9 @@ def add_random_gaussian(a): fig, ax = plt.subplots() ax.stackplot(x, ys, baseline='wiggle') plt.show() + +# %% +# .. tags:: +# +# plot-type: stackplot +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/stairs_demo.py b/galleries/examples/lines_bars_and_markers/stairs_demo.py index 223e8c2aa1e5..9c7506e52b27 100644 --- a/galleries/examples/lines_bars_and_markers/stairs_demo.py +++ b/galleries/examples/lines_bars_and_markers/stairs_demo.py @@ -90,3 +90,8 @@ # # - `matplotlib.axes.Axes.stairs` / `matplotlib.pyplot.stairs` # - `matplotlib.patches.StepPatch` +# +# .. tags:: +# +# plot-type: stairs +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/stem_plot.py b/galleries/examples/lines_bars_and_markers/stem_plot.py index f3035c1673e6..cde8fd8e8017 100644 --- a/galleries/examples/lines_bars_and_markers/stem_plot.py +++ b/galleries/examples/lines_bars_and_markers/stem_plot.py @@ -1,6 +1,6 @@ """ ========= -Stem Plot +Stem plot ========= `~.pyplot.stem` plots vertical lines from a baseline to the y-coordinate and @@ -36,3 +36,8 @@ # in this example: # # - `matplotlib.axes.Axes.stem` / `matplotlib.pyplot.stem` +# +# .. tags:: +# +# plot-type: stem +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/step_demo.py b/galleries/examples/lines_bars_and_markers/step_demo.py index 97d2a37eb4c6..f74a069e52f3 100644 --- a/galleries/examples/lines_bars_and_markers/step_demo.py +++ b/galleries/examples/lines_bars_and_markers/step_demo.py @@ -63,3 +63,9 @@ # # - `matplotlib.axes.Axes.step` / `matplotlib.pyplot.step` # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` +# +# .. tags:: +# +# plot-type: step +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/timeline.py b/galleries/examples/lines_bars_and_markers/timeline.py index 93b98a403620..55f90d5103d2 100644 --- a/galleries/examples/lines_bars_and_markers/timeline.py +++ b/galleries/examples/lines_bars_and_markers/timeline.py @@ -1,7 +1,7 @@ """ -=============================================== -Creating a timeline with lines, dates, and text -=============================================== +==================================== +Timeline with lines, dates, and text +==================================== How to create a simple timeline using Matplotlib release dates. @@ -49,6 +49,7 @@ '2014-10-26', '2014-10-18', '2014-08-26'] dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates] # Convert strs to dates. +releases = [tuple(release.split('.')) for release in releases] # Split by component. dates, releases = zip(*sorted(zip(dates, releases))) # Sort by increasing date. # %% @@ -61,42 +62,49 @@ # # Note that Matplotlib will automatically plot datetime inputs. -# Choose some nice levels: alternate minor releases between top and bottom, and -# progressievly shorten the stems for bugfix releases. +# Choose some nice levels: alternate meso releases between top and bottom, and +# progressively shorten the stems for micro releases. levels = [] -major_minor_releases = sorted({release[:3] for release in releases}) +macro_meso_releases = sorted({release[:2] for release in releases}) for release in releases: - major_minor = release[:3] - bugfix = int(release[4]) - h = 1 + 0.8 * (5 - bugfix) - level = h if major_minor_releases.index(major_minor) % 2 == 0 else -h + macro_meso = release[:2] + micro = int(release[2]) + h = 1 + 0.8 * (5 - micro) + level = h if macro_meso_releases.index(macro_meso) % 2 == 0 else -h levels.append(level) + +def is_feature(release): + """Return whether a version (split into components) is a feature release.""" + return release[-1] == '0' + + # The figure and the axes. fig, ax = plt.subplots(figsize=(8.8, 4), layout="constrained") ax.set(title="Matplotlib release dates") # The vertical stems. ax.vlines(dates, 0, levels, - color=[("tab:red", 1 if release.endswith(".0") else .5) - for release in releases]) + color=[("tab:red", 1 if is_feature(release) else .5) for release in releases]) # The baseline. ax.axhline(0, c="black") # The markers on the baseline. -minor_dates = [date for date, release in zip(dates, releases) if release[-1] == '0'] -bugfix_dates = [date for date, release in zip(dates, releases) if release[-1] != '0'] -ax.plot(bugfix_dates, np.zeros_like(bugfix_dates), "ko", mfc="white") -ax.plot(minor_dates, np.zeros_like(minor_dates), "ko", mfc="tab:red") +meso_dates = [date for date, release in zip(dates, releases) if is_feature(release)] +micro_dates = [date for date, release in zip(dates, releases) + if not is_feature(release)] +ax.plot(micro_dates, np.zeros_like(micro_dates), "ko", mfc="white") +ax.plot(meso_dates, np.zeros_like(meso_dates), "ko", mfc="tab:red") # Annotate the lines. for date, level, release in zip(dates, levels, releases): - ax.annotate(release, xy=(date, level), + version_str = '.'.join(release) + ax.annotate(version_str, xy=(date, level), xytext=(-3, np.sign(level)*3), textcoords="offset points", verticalalignment="bottom" if level > 0 else "top", - weight="bold" if release.endswith(".0") else "normal", + weight="bold" if is_feature(release) else "normal", bbox=dict(boxstyle='square', pad=0, lw=0, fc=(1, 1, 1, 0.7))) -ax.yaxis.set(major_locator=mdates.YearLocator(), +ax.xaxis.set(major_locator=mdates.YearLocator(), major_formatter=mdates.DateFormatter("%Y")) # Remove the y-axis and some spines. @@ -120,3 +128,9 @@ # - `matplotlib.axis.Axis.set_major_formatter` # - `matplotlib.dates.MonthLocator` # - `matplotlib.dates.DateFormatter` +# +# .. tags:: +# +# component: annotate +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/vline_hline_demo.py b/galleries/examples/lines_bars_and_markers/vline_hline_demo.py index c2f5d025b15c..4bec4be760ee 100644 --- a/galleries/examples/lines_bars_and_markers/vline_hline_demo.py +++ b/galleries/examples/lines_bars_and_markers/vline_hline_demo.py @@ -32,3 +32,9 @@ hax.set_title('Horizontal lines demo') plt.show() + +# %% +# .. tags:: +# +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py b/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py index eff0d7269a49..7878ef8d7468 100644 --- a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py +++ b/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py @@ -34,3 +34,8 @@ # # - `matplotlib.axes.Axes.acorr` / `matplotlib.pyplot.acorr` # - `matplotlib.axes.Axes.xcorr` / `matplotlib.pyplot.xcorr` +# +# .. tags:: +# +# domain: statistics +# level: beginner diff --git a/galleries/examples/misc/bbox_intersect.py b/galleries/examples/misc/bbox_intersect.py index c645cd34c155..9103705537d5 100644 --- a/galleries/examples/misc/bbox_intersect.py +++ b/galleries/examples/misc/bbox_intersect.py @@ -1,7 +1,7 @@ """ -=========================================== -Changing colors of lines intersecting a box -=========================================== +================================== +Identify whether artists intersect +================================== The lines intersecting the rectangle are colored in red, while the others are left as blue lines. This example showcases the `.intersects_bbox` function. diff --git a/galleries/examples/misc/demo_ribbon_box.py b/galleries/examples/misc/demo_ribbon_box.py index d5121ba6ff5c..5400a2a0063e 100644 --- a/galleries/examples/misc/demo_ribbon_box.py +++ b/galleries/examples/misc/demo_ribbon_box.py @@ -1,6 +1,6 @@ """ ========== -Ribbon Box +Ribbon box ========== """ diff --git a/galleries/examples/misc/fig_x.py b/galleries/examples/misc/fig_x.py index e2af3e766028..593a7e8f8aa5 100644 --- a/galleries/examples/misc/fig_x.py +++ b/galleries/examples/misc/fig_x.py @@ -1,9 +1,10 @@ """ -======================= -Adding lines to figures -======================= +============================== +Add lines directly to a figure +============================== -Adding lines to a figure without any Axes. +You can add artists such as a `.Line2D` directly to a figure. This is +typically useful for visual structuring. .. redirect-from:: /gallery/pyplots/fig_x """ @@ -12,9 +13,9 @@ import matplotlib.lines as lines -fig = plt.figure() -fig.add_artist(lines.Line2D([0, 1], [0, 1])) -fig.add_artist(lines.Line2D([0, 1], [1, 0])) +fig, axs = plt.subplots(2, 2, gridspec_kw={'hspace': 0.4, 'wspace': 0.4}) +fig.add_artist(lines.Line2D([0, 1], [0.47, 0.47], linewidth=3)) +fig.add_artist(lines.Line2D([0.5, 0.5], [1, 0], linewidth=3)) plt.show() # %% diff --git a/galleries/examples/misc/fill_spiral.py b/galleries/examples/misc/fill_spiral.py index e82f0203e39f..35b06886e985 100644 --- a/galleries/examples/misc/fill_spiral.py +++ b/galleries/examples/misc/fill_spiral.py @@ -1,6 +1,6 @@ """ =========== -Fill Spiral +Fill spiral =========== """ diff --git a/galleries/examples/misc/font_indexing.py b/galleries/examples/misc/font_indexing.py index 02d77e647bf3..31388737bcae 100644 --- a/galleries/examples/misc/font_indexing.py +++ b/galleries/examples/misc/font_indexing.py @@ -9,8 +9,7 @@ import os import matplotlib -from matplotlib.ft2font import (KERNING_DEFAULT, KERNING_UNFITTED, - KERNING_UNSCALED, FT2Font) +from matplotlib.ft2font import FT2Font, Kerning font = FT2Font( os.path.join(matplotlib.get_data_path(), 'fonts/ttf/DejaVuSans.ttf')) @@ -31,7 +30,7 @@ glyph = font.load_char(code) print(glyph.bbox) print(glyphd['A'], glyphd['V'], coded['A'], coded['V']) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_DEFAULT)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNFITTED)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNSCALED)) -print('AT', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.DEFAULT)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNFITTED)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNSCALED)) +print('AT', font.get_kerning(glyphd['A'], glyphd['T'], Kerning.UNSCALED)) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index 0cdf363fb6ba..8306e142c144 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -46,18 +46,10 @@ # vertical thickness of the underline print('Underline thickness:', font.underline_thickness) -for style in ('Italic', - 'Bold', - 'Scalable', - 'Fixed sizes', - 'Fixed width', - 'SFNT', - 'Horizontal', - 'Vertical', - 'Kerning', - 'Fast glyphs', - 'Multiple masters', - 'Glyph names', - 'External stream'): - bitpos = getattr(ft, style.replace(' ', '_').upper()) - 1 - print(f"{style+':':17}", bool(font.style_flags & (1 << bitpos))) +for flag in ft.StyleFlags: + name = flag.name.replace('_', ' ').title() + ':' + print(f"{name:17}", flag in font.style_flags) + +for flag in ft.FaceFlags: + name = flag.name.replace('_', ' ').title() + ':' + print(f"{name:17}", flag in font.face_flags) diff --git a/galleries/examples/misc/image_thumbnail_sgskip.py b/galleries/examples/misc/image_thumbnail_sgskip.py index edc1e5aa3573..e361d3bf53ab 100644 --- a/galleries/examples/misc/image_thumbnail_sgskip.py +++ b/galleries/examples/misc/image_thumbnail_sgskip.py @@ -1,13 +1,13 @@ """ =============== -Image Thumbnail +Image thumbnail =============== You can use Matplotlib to generate thumbnails from existing images. Matplotlib relies on Pillow_ for reading images, and thus supports all formats supported by Pillow. -.. _Pillow: https://python-pillow.org/ +.. _Pillow: https://python-pillow.github.io """ from argparse import ArgumentParser diff --git a/galleries/examples/misc/print_stdout_sgskip.py b/galleries/examples/misc/print_stdout_sgskip.py index 4a8b63f6d03e..9c9848a73d9c 100644 --- a/galleries/examples/misc/print_stdout_sgskip.py +++ b/galleries/examples/misc/print_stdout_sgskip.py @@ -1,7 +1,7 @@ """ -============ -Print Stdout -============ +===================== +Print image to stdout +===================== print png to standard out diff --git a/galleries/examples/misc/svg_filter_line.py b/galleries/examples/misc/svg_filter_line.py index 5cc4af5d7a66..c6adec093bee 100644 --- a/galleries/examples/misc/svg_filter_line.py +++ b/galleries/examples/misc/svg_filter_line.py @@ -1,7 +1,7 @@ """ -=============== -SVG Filter Line -=============== +========================== +Apply SVG filter to a line +========================== Demonstrate SVG filtering effects which might be used with Matplotlib. diff --git a/galleries/examples/mplot3d/2dcollections3d.py b/galleries/examples/mplot3d/2dcollections3d.py index a0155ebb0773..ae88776d133e 100644 --- a/galleries/examples/mplot3d/2dcollections3d.py +++ b/galleries/examples/mplot3d/2dcollections3d.py @@ -46,3 +46,9 @@ ax.view_init(elev=20., azim=-35, roll=0) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: scatter, plot-type: line, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/3d_bars.py b/galleries/examples/mplot3d/3d_bars.py index 40a09ae33f68..9d8feeaeb12b 100644 --- a/galleries/examples/mplot3d/3d_bars.py +++ b/galleries/examples/mplot3d/3d_bars.py @@ -31,3 +31,10 @@ ax2.set_title('Not Shaded') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: texture, +# plot-type: bar, +# level: beginner diff --git a/galleries/examples/mplot3d/axlim_clip.py b/galleries/examples/mplot3d/axlim_clip.py new file mode 100644 index 000000000000..2a29f2bf2431 --- /dev/null +++ b/galleries/examples/mplot3d/axlim_clip.py @@ -0,0 +1,40 @@ +""" +===================================== +Clip the data to the axes view limits +===================================== + +Demonstrate clipping of line and marker data to the axes view limits. The +``axlim_clip`` keyword argument can be used in any of the 3D plotting +functions. +""" + +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + +# Make the data +x = np.arange(-5, 5, 0.5) +y = np.arange(-5, 5, 0.5) +X, Y = np.meshgrid(x, y) +R = np.sqrt(X**2 + Y**2) +Z = np.sin(R) + +# Default behavior is axlim_clip=False +ax.plot_wireframe(X, Y, Z, color='C0') + +# When axlim_clip=True, note that when a line segment has one vertex outside +# the view limits, the entire line is hidden. The same is true for 3D patches +# if one of their vertices is outside the limits (not shown). +ax.plot_wireframe(X, Y, Z, color='C1', axlim_clip=True) + +# In this example, data where x < 0 or z > 0.5 is clipped +ax.set(xlim=(0, 10), ylim=(-5, 5), zlim=(-1, 0.5)) +ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) + +plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/bars3d.py b/galleries/examples/mplot3d/bars3d.py index 21314057311a..3ea4a100c2f6 100644 --- a/galleries/examples/mplot3d/bars3d.py +++ b/galleries/examples/mplot3d/bars3d.py @@ -40,3 +40,9 @@ ax.set_yticks(yticks) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: bar, +# styling: color, +# level: beginner diff --git a/galleries/examples/mplot3d/box3d.py b/galleries/examples/mplot3d/box3d.py index bbe4accec183..807e3d496ec6 100644 --- a/galleries/examples/mplot3d/box3d.py +++ b/galleries/examples/mplot3d/box3d.py @@ -76,3 +76,8 @@ # Show Figure plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/contour3d.py b/galleries/examples/mplot3d/contour3d.py index fb2e5bb5a30d..6ac98bc47ab1 100644 --- a/galleries/examples/mplot3d/contour3d.py +++ b/galleries/examples/mplot3d/contour3d.py @@ -18,3 +18,8 @@ ax.contour(X, Y, Z, cmap=cm.coolwarm) # Plot contour curves plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contour3d_2.py b/galleries/examples/mplot3d/contour3d_2.py index 1283deb27c81..0f1aac1450a8 100644 --- a/galleries/examples/mplot3d/contour3d_2.py +++ b/galleries/examples/mplot3d/contour3d_2.py @@ -17,3 +17,8 @@ ax.contour(X, Y, Z, extend3d=True, cmap=cm.coolwarm) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contour3d_3.py b/galleries/examples/mplot3d/contour3d_3.py index 6f73fea85dcb..92adb97fc04e 100644 --- a/galleries/examples/mplot3d/contour3d_3.py +++ b/galleries/examples/mplot3d/contour3d_3.py @@ -29,3 +29,9 @@ xlabel='X', ylabel='Y', zlabel='Z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/contourf3d.py b/galleries/examples/mplot3d/contourf3d.py index 9f7157eab82a..2512179c3e54 100644 --- a/galleries/examples/mplot3d/contourf3d.py +++ b/galleries/examples/mplot3d/contourf3d.py @@ -20,3 +20,8 @@ ax.contourf(X, Y, Z, cmap=cm.coolwarm) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contourf3d_2.py b/galleries/examples/mplot3d/contourf3d_2.py index 1530aee5e87f..58fede4e3ab5 100644 --- a/galleries/examples/mplot3d/contourf3d_2.py +++ b/galleries/examples/mplot3d/contourf3d_2.py @@ -29,3 +29,9 @@ xlabel='X', ylabel='Y', zlabel='Z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/custom_shaded_3d_surface.py b/galleries/examples/mplot3d/custom_shaded_3d_surface.py index 677bfa179a83..1a9fa8d4f7eb 100644 --- a/galleries/examples/mplot3d/custom_shaded_3d_surface.py +++ b/galleries/examples/mplot3d/custom_shaded_3d_surface.py @@ -34,3 +34,9 @@ linewidth=0, antialiased=False, shade=False) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate, +# domain: cartography diff --git a/galleries/examples/mplot3d/errorbar3d.py b/galleries/examples/mplot3d/errorbar3d.py index e4da658d194b..1ece3ca1e8cf 100644 --- a/galleries/examples/mplot3d/errorbar3d.py +++ b/galleries/examples/mplot3d/errorbar3d.py @@ -27,3 +27,9 @@ ax.set_zlabel("Z label") plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: error, +# level: beginner diff --git a/galleries/examples/mplot3d/fillbetween3d.py b/galleries/examples/mplot3d/fillbetween3d.py new file mode 100644 index 000000000000..b9d61b4d1eb2 --- /dev/null +++ b/galleries/examples/mplot3d/fillbetween3d.py @@ -0,0 +1,34 @@ +""" +===================== +Fill between 3D lines +===================== + +Demonstrate how to fill the space between 3D lines with surfaces. Here we +create a sort of "lampshade" shape. +""" + +import matplotlib.pyplot as plt +import numpy as np + +N = 50 +theta = np.linspace(0, 2*np.pi, N) + +x1 = np.cos(theta) +y1 = np.sin(theta) +z1 = 0.1 * np.sin(6 * theta) + +x2 = 0.6 * np.cos(theta) +y2 = 0.6 * np.sin(theta) +z2 = 2 # Note that scalar values work in addition to length N arrays + +fig = plt.figure() +ax = fig.add_subplot(projection='3d') +ax.fill_between(x1, y1, z1, x2, y2, z2, alpha=0.5, edgecolor='k') + +plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# plot-type: fill_between, +# level: beginner diff --git a/galleries/examples/mplot3d/fillunder3d.py b/galleries/examples/mplot3d/fillunder3d.py new file mode 100644 index 000000000000..7e9889633f70 --- /dev/null +++ b/galleries/examples/mplot3d/fillunder3d.py @@ -0,0 +1,40 @@ +""" +========================= +Fill under 3D line graphs +========================= + +Demonstrate how to create polygons which fill the space under a line +graph. In this example polygons are semi-transparent, creating a sort +of 'jagged stained glass' effect. +""" + +import math + +import matplotlib.pyplot as plt +import numpy as np + +gamma = np.vectorize(math.gamma) +N = 31 +x = np.linspace(0., 10., N) +lambdas = range(1, 9) + +ax = plt.figure().add_subplot(projection='3d') + +facecolors = plt.colormaps['viridis_r'](np.linspace(0, 1, len(lambdas))) + +for i, l in enumerate(lambdas): + # Note fill_between can take coordinates as length N vectors, or scalars + ax.fill_between(x, l, l**x * np.exp(-l) / gamma(x + 1), + x, l, 0, + facecolors=facecolors[i], alpha=.7) + +ax.set(xlim=(0, 10), ylim=(1, 9), zlim=(0, 0.35), + xlabel='x', ylabel=r'$\lambda$', zlabel='probability') + +plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# plot-type: fill_between, +# level: beginner diff --git a/galleries/examples/mplot3d/hist3d.py b/galleries/examples/mplot3d/hist3d.py index e602f7f1e6c5..65d0d60958d8 100644 --- a/galleries/examples/mplot3d/hist3d.py +++ b/galleries/examples/mplot3d/hist3d.py @@ -31,3 +31,9 @@ ax.bar3d(xpos, ypos, zpos, dx, dy, dz, zsort='average') plt.show() + + +# %% +# .. tags:: +# plot-type: 3D, plot-type: histogram, +# level: beginner diff --git a/galleries/examples/mplot3d/imshow3d.py b/galleries/examples/mplot3d/imshow3d.py index 557d96e1bce5..dba962734bbe 100644 --- a/galleries/examples/mplot3d/imshow3d.py +++ b/galleries/examples/mplot3d/imshow3d.py @@ -86,3 +86,9 @@ def imshow3d(ax, array, value_direction='z', pos=0, norm=None, cmap=None): imshow3d(ax, data_zx, value_direction='y', pos=ny, cmap='plasma') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: advanced diff --git a/galleries/examples/mplot3d/intersecting_planes.py b/galleries/examples/mplot3d/intersecting_planes.py index b8aa08fd7e18..a5a92caf5c6b 100644 --- a/galleries/examples/mplot3d/intersecting_planes.py +++ b/galleries/examples/mplot3d/intersecting_planes.py @@ -87,3 +87,9 @@ def figure_3D_array_slices(array, cmap=None): figure_3D_array_slices(r_square, cmap='viridis_r') plt.show() + + +# %% +# .. tags:: +# plot-type: 3D, +# level: advanced diff --git a/galleries/examples/mplot3d/lines3d.py b/galleries/examples/mplot3d/lines3d.py index 2fe3b8f30177..ee38dade6997 100644 --- a/galleries/examples/mplot3d/lines3d.py +++ b/galleries/examples/mplot3d/lines3d.py @@ -22,3 +22,8 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/lorenz_attractor.py b/galleries/examples/mplot3d/lorenz_attractor.py index 0ac54a7adb9b..72d25ea544cb 100644 --- a/galleries/examples/mplot3d/lorenz_attractor.py +++ b/galleries/examples/mplot3d/lorenz_attractor.py @@ -59,3 +59,8 @@ def lorenz(xyz, *, s=10, r=28, b=2.667): ax.set_title("Lorenz Attractor") plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/mixed_subplots.py b/galleries/examples/mplot3d/mixed_subplots.py index dc196f05f90d..a38fd2e10a2b 100644 --- a/galleries/examples/mplot3d/mixed_subplots.py +++ b/galleries/examples/mplot3d/mixed_subplots.py @@ -44,3 +44,9 @@ def f(t): ax.set_zlim(-1, 1) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: subplot, +# level: beginner diff --git a/galleries/examples/mplot3d/offset.py b/galleries/examples/mplot3d/offset.py index 78da5c6b51c3..4c5e4b06b62b 100644 --- a/galleries/examples/mplot3d/offset.py +++ b/galleries/examples/mplot3d/offset.py @@ -29,3 +29,10 @@ ax.set_zlim(0, 2) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: label, +# interactivity: pan, +# level: beginner diff --git a/galleries/examples/mplot3d/pathpatch3d.py b/galleries/examples/mplot3d/pathpatch3d.py index 335b68003d31..8cb7c4951809 100644 --- a/galleries/examples/mplot3d/pathpatch3d.py +++ b/galleries/examples/mplot3d/pathpatch3d.py @@ -69,3 +69,9 @@ def text3d(ax, xyz, s, zdir="z", size=None, angle=0, usetex=False, **kwargs): ax.set_zlim(0, 10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: label, +# level: advanced diff --git a/galleries/examples/mplot3d/polys3d.py b/galleries/examples/mplot3d/polys3d.py index b174f804d61d..19979ceddaa5 100644 --- a/galleries/examples/mplot3d/polys3d.py +++ b/galleries/examples/mplot3d/polys3d.py @@ -1,47 +1,41 @@ """ -============================================= -Generate polygons to fill under 3D line graph -============================================= +==================== +Generate 3D polygons +==================== -Demonstrate how to create polygons which fill the space under a line -graph. In this example polygons are semi-transparent, creating a sort -of 'jagged stained glass' effect. +Demonstrate how to create polygons in 3D. Here we stack 3 hexagons. """ -import math - import matplotlib.pyplot as plt import numpy as np -from matplotlib.collections import PolyCollection - -# Fixing random state for reproducibility -np.random.seed(19680801) +from mpl_toolkits.mplot3d.art3d import Poly3DCollection +# Coordinates of a hexagon +angles = np.linspace(0, 2 * np.pi, 6, endpoint=False) +x = np.cos(angles) +y = np.sin(angles) +zs = [-3, -2, -1] -def polygon_under_graph(x, y): - """ - Construct the vertex list which defines the polygon filling the space under - the (x, y) line graph. This assumes x is in ascending order. - """ - return [(x[0], 0.), *zip(x, y), (x[-1], 0.)] +# Close the hexagon by repeating the first vertex +x = np.append(x, x[0]) +y = np.append(y, y[0]) +verts = [] +for z in zs: + verts.append(list(zip(x*z, y*z, np.full_like(x, z)))) +verts = np.array(verts) ax = plt.figure().add_subplot(projection='3d') -x = np.linspace(0., 10., 31) -lambdas = range(1, 9) - -# verts[i] is a list of (x, y) pairs defining polygon i. -gamma = np.vectorize(math.gamma) -verts = [polygon_under_graph(x, l**x * np.exp(-l) / gamma(x + 1)) - for l in lambdas] -facecolors = plt.colormaps['viridis_r'](np.linspace(0, 1, len(verts))) - -poly = PolyCollection(verts, facecolors=facecolors, alpha=.7) -ax.add_collection3d(poly, zs=lambdas, zdir='y') - -ax.set(xlim=(0, 10), ylim=(1, 9), zlim=(0, 0.35), - xlabel='x', ylabel=r'$\lambda$', zlabel='probability') +poly = Poly3DCollection(verts, alpha=.7) +ax.add_collection3d(poly) +ax.set_aspect('equalxy') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: intermediate diff --git a/galleries/examples/mplot3d/projections.py b/galleries/examples/mplot3d/projections.py index 4fdeb6729687..ff9d88ccb5cd 100644 --- a/galleries/examples/mplot3d/projections.py +++ b/galleries/examples/mplot3d/projections.py @@ -53,3 +53,10 @@ axs[2].set_title("'persp'\nfocal_length = 0.2", fontsize=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: small-multiples, +# component: subplot, +# level: intermediate diff --git a/galleries/examples/mplot3d/quiver3d.py b/galleries/examples/mplot3d/quiver3d.py index 1eba869c83b8..adc58c2e9d89 100644 --- a/galleries/examples/mplot3d/quiver3d.py +++ b/galleries/examples/mplot3d/quiver3d.py @@ -25,3 +25,8 @@ ax.quiver(x, y, z, u, v, w, length=0.1, normalize=True) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/rotate_axes3d_sgskip.py b/galleries/examples/mplot3d/rotate_axes3d_sgskip.py index 4474fab97460..76a3369a20d6 100644 --- a/galleries/examples/mplot3d/rotate_axes3d_sgskip.py +++ b/galleries/examples/mplot3d/rotate_axes3d_sgskip.py @@ -49,3 +49,10 @@ plt.draw() plt.pause(.001) + +# %% +# .. tags:: +# plot-type: 3D, +# component: animation, +# level: advanced, +# internal: high-bandwidth diff --git a/galleries/examples/mplot3d/scatter3d.py b/galleries/examples/mplot3d/scatter3d.py index 6db0ac9222bc..0fc9bf3fe8da 100644 --- a/galleries/examples/mplot3d/scatter3d.py +++ b/galleries/examples/mplot3d/scatter3d.py @@ -38,3 +38,8 @@ def randrange(n, vmin, vmax): ax.set_zlabel('Z Label') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: scatter, +# level: beginner diff --git a/galleries/examples/mplot3d/stem3d_demo.py b/galleries/examples/mplot3d/stem3d_demo.py index 6f1773c1b505..6e45e7e75c72 100644 --- a/galleries/examples/mplot3d/stem3d_demo.py +++ b/galleries/examples/mplot3d/stem3d_demo.py @@ -49,3 +49,8 @@ ax.set(xlabel='x', ylabel='y', zlabel='z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: speciality, +# level: beginner diff --git a/galleries/examples/mplot3d/subplot3d.py b/galleries/examples/mplot3d/subplot3d.py index 47e374dc74b9..67d6a81b9e87 100644 --- a/galleries/examples/mplot3d/subplot3d.py +++ b/galleries/examples/mplot3d/subplot3d.py @@ -43,3 +43,9 @@ ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: subplot, +# level: advanced diff --git a/galleries/examples/mplot3d/surface3d.py b/galleries/examples/mplot3d/surface3d.py index e92e6aabd52c..d8a67d40d8b8 100644 --- a/galleries/examples/mplot3d/surface3d.py +++ b/galleries/examples/mplot3d/surface3d.py @@ -53,3 +53,8 @@ # - `matplotlib.axis.Axis.set_major_locator` # - `matplotlib.ticker.LinearLocator` # - `matplotlib.ticker.StrMethodFormatter` +# +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: advanced diff --git a/galleries/examples/mplot3d/surface3d_2.py b/galleries/examples/mplot3d/surface3d_2.py index 37ca667d688a..2a4406abc259 100644 --- a/galleries/examples/mplot3d/surface3d_2.py +++ b/galleries/examples/mplot3d/surface3d_2.py @@ -26,3 +26,8 @@ ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/surface3d_3.py b/galleries/examples/mplot3d/surface3d_3.py index a2aca4ca3059..c129ef6d3635 100644 --- a/galleries/examples/mplot3d/surface3d_3.py +++ b/galleries/examples/mplot3d/surface3d_3.py @@ -38,3 +38,9 @@ ax.zaxis.set_major_locator(LinearLocator(6)) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color, styling: texture, +# level: intermediate diff --git a/galleries/examples/mplot3d/surface3d_radial.py b/galleries/examples/mplot3d/surface3d_radial.py index 0d27c9b58cbb..43edd68ee28e 100644 --- a/galleries/examples/mplot3d/surface3d_radial.py +++ b/galleries/examples/mplot3d/surface3d_radial.py @@ -35,3 +35,8 @@ ax.set_zlabel(r'$V(\phi)$') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: polar, +# level: beginner diff --git a/galleries/examples/mplot3d/text3d.py b/galleries/examples/mplot3d/text3d.py index 165ae556c334..881ecfaf406e 100644 --- a/galleries/examples/mplot3d/text3d.py +++ b/galleries/examples/mplot3d/text3d.py @@ -44,3 +44,9 @@ ax.set_zlabel('Z axis') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: annotation, +# level: beginner diff --git a/galleries/examples/mplot3d/tricontour3d.py b/galleries/examples/mplot3d/tricontour3d.py index abf72103d098..fda8de784d71 100644 --- a/galleries/examples/mplot3d/tricontour3d.py +++ b/galleries/examples/mplot3d/tricontour3d.py @@ -43,3 +43,8 @@ ax.view_init(elev=45.) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/tricontourf3d.py b/galleries/examples/mplot3d/tricontourf3d.py index 94cee6b3aaa9..edf79495e374 100644 --- a/galleries/examples/mplot3d/tricontourf3d.py +++ b/galleries/examples/mplot3d/tricontourf3d.py @@ -44,3 +44,8 @@ ax.view_init(elev=45.) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/trisurf3d.py b/galleries/examples/mplot3d/trisurf3d.py index 2d288908ab69..f4e7444a4311 100644 --- a/galleries/examples/mplot3d/trisurf3d.py +++ b/galleries/examples/mplot3d/trisurf3d.py @@ -30,3 +30,8 @@ ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/trisurf3d_2.py b/galleries/examples/mplot3d/trisurf3d_2.py index cb53aabbea1d..b04aa5efb0b1 100644 --- a/galleries/examples/mplot3d/trisurf3d_2.py +++ b/galleries/examples/mplot3d/trisurf3d_2.py @@ -77,3 +77,8 @@ plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/view_planes_3d.py b/galleries/examples/mplot3d/view_planes_3d.py index c4322d60fe93..1cac9d61ad1f 100644 --- a/galleries/examples/mplot3d/view_planes_3d.py +++ b/galleries/examples/mplot3d/view_planes_3d.py @@ -55,3 +55,9 @@ def annotate_axes(ax, text, fontsize=18): axd['L'].set_axis_off() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, component: subplot, +# level: beginner diff --git a/galleries/examples/mplot3d/voxels.py b/galleries/examples/mplot3d/voxels.py index 7bd9cf45a2b0..ec9f0f413f3a 100644 --- a/galleries/examples/mplot3d/voxels.py +++ b/galleries/examples/mplot3d/voxels.py @@ -32,3 +32,8 @@ ax.voxels(voxelarray, facecolors=colors, edgecolor='k') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/voxels_numpy_logo.py b/galleries/examples/mplot3d/voxels_numpy_logo.py index 34eb48dcbe8a..c128f055cbe6 100644 --- a/galleries/examples/mplot3d/voxels_numpy_logo.py +++ b/galleries/examples/mplot3d/voxels_numpy_logo.py @@ -45,3 +45,9 @@ def explode(data): ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner, +# purpose: fun diff --git a/galleries/examples/mplot3d/voxels_rgb.py b/galleries/examples/mplot3d/voxels_rgb.py index 3ee1e1eab1a6..6f201b08b386 100644 --- a/galleries/examples/mplot3d/voxels_rgb.py +++ b/galleries/examples/mplot3d/voxels_rgb.py @@ -42,3 +42,8 @@ def midpoints(x): ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color diff --git a/galleries/examples/mplot3d/voxels_torus.py b/galleries/examples/mplot3d/voxels_torus.py index 98621b60976c..db0fdbc6ea4d 100644 --- a/galleries/examples/mplot3d/voxels_torus.py +++ b/galleries/examples/mplot3d/voxels_torus.py @@ -44,3 +44,9 @@ def midpoints(x): linewidth=0.5) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color, +# level: intermediate diff --git a/galleries/examples/mplot3d/wire3d.py b/galleries/examples/mplot3d/wire3d.py index 9849c8bebf56..357234f51174 100644 --- a/galleries/examples/mplot3d/wire3d.py +++ b/galleries/examples/mplot3d/wire3d.py @@ -20,3 +20,8 @@ ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/wire3d_animation_sgskip.py b/galleries/examples/mplot3d/wire3d_animation_sgskip.py index a735f41d94b6..903ff4918586 100644 --- a/galleries/examples/mplot3d/wire3d_animation_sgskip.py +++ b/galleries/examples/mplot3d/wire3d_animation_sgskip.py @@ -39,3 +39,9 @@ plt.pause(.001) print('Average FPS: %f' % (100 / (time.time() - tstart))) + +# %% +# .. tags:: +# plot-type: 3D, +# component: animation, +# level: beginner diff --git a/galleries/examples/mplot3d/wire3d_zero_stride.py b/galleries/examples/mplot3d/wire3d_zero_stride.py index fe45b6c16fcf..ff6a14984b5d 100644 --- a/galleries/examples/mplot3d/wire3d_zero_stride.py +++ b/galleries/examples/mplot3d/wire3d_zero_stride.py @@ -27,3 +27,8 @@ plt.tight_layout() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate 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 ef68b3d79971..6f18b964cef7 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -81,3 +81,11 @@ # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` # - `matplotlib.patches.ConnectionPatch` +# +# .. tags:: +# +# component: subplot +# plot-type: pie +# plot-type: bar +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/pie_and_polar_charts/nested_pie.py b/galleries/examples/pie_and_polar_charts/nested_pie.py index 61cb5e6ee429..699360a1e3fa 100644 --- a/galleries/examples/pie_and_polar_charts/nested_pie.py +++ b/galleries/examples/pie_and_polar_charts/nested_pie.py @@ -90,3 +90,9 @@ # - `matplotlib.projections.polar` # - ``Axes.set`` (`matplotlib.artist.Artist.set`) # - `matplotlib.axes.Axes.set_axis_off` +# +# .. tags:: +# +# plot-type: pie +# level: beginner +# purpose: showcase 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 ae9b805cf005..13e3019bc7ba 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 @@ -1,7 +1,7 @@ """ -========================== -Labeling a pie and a donut -========================== +============================= +A pie and a donut with labels +============================= Welcome to the Matplotlib bakery. We will create a pie and a donut chart through the `pie method ` and @@ -132,3 +132,10 @@ def func(pct, allvals): # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` +# +# .. tags:: +# +# component: label +# component: annotation +# plot-type: pie +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/pie_features.py b/galleries/examples/pie_and_polar_charts/pie_features.py index 7794a3d22a7e..47781a31a373 100644 --- a/galleries/examples/pie_and_polar_charts/pie_features.py +++ b/galleries/examples/pie_and_polar_charts/pie_features.py @@ -130,3 +130,8 @@ # in this example: # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# +# .. tags:: +# +# plot-type: pie +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_bar.py b/galleries/examples/pie_and_polar_charts/polar_bar.py index 750032c8710d..ba0a3c25fd40 100644 --- a/galleries/examples/pie_and_polar_charts/polar_bar.py +++ b/galleries/examples/pie_and_polar_charts/polar_bar.py @@ -32,3 +32,10 @@ # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.projections.polar` +# +# .. tags:: +# +# plot-type: pie +# plot-type: bar +# level: beginner +# purpose: showcase diff --git a/galleries/examples/pie_and_polar_charts/polar_demo.py b/galleries/examples/pie_and_polar_charts/polar_demo.py index 75a7d61f6244..e4967079d19d 100644 --- a/galleries/examples/pie_and_polar_charts/polar_demo.py +++ b/galleries/examples/pie_and_polar_charts/polar_demo.py @@ -34,3 +34,8 @@ # - `matplotlib.projections.polar.PolarAxes.set_rticks` # - `matplotlib.projections.polar.PolarAxes.set_rmax` # - `matplotlib.projections.polar.PolarAxes.set_rlabel_position` +# +# .. tags:: +# +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_error_caps.py b/galleries/examples/pie_and_polar_charts/polar_error_caps.py index aa950e40613a..7f77a2c48834 100644 --- a/galleries/examples/pie_and_polar_charts/polar_error_caps.py +++ b/galleries/examples/pie_and_polar_charts/polar_error_caps.py @@ -51,3 +51,10 @@ # # - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar` # - `matplotlib.projections.polar` +# +# .. tags:: +# +# component: error +# plot-type: errorbar +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_legend.py b/galleries/examples/pie_and_polar_charts/polar_legend.py index 7972b0aaffd4..cef4bc8ccef6 100644 --- a/galleries/examples/pie_and_polar_charts/polar_legend.py +++ b/galleries/examples/pie_and_polar_charts/polar_legend.py @@ -38,3 +38,9 @@ # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # - `matplotlib.projections.polar` # - `matplotlib.projections.polar.PolarAxes` +# +# .. tags:: +# +# component: legend +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_scatter.py b/galleries/examples/pie_and_polar_charts/polar_scatter.py index c36d74966805..af7dff04f195 100644 --- a/galleries/examples/pie_and_polar_charts/polar_scatter.py +++ b/galleries/examples/pie_and_polar_charts/polar_scatter.py @@ -67,3 +67,9 @@ # - `matplotlib.projections.polar.PolarAxes.set_theta_zero_location` # - `matplotlib.projections.polar.PolarAxes.set_thetamin` # - `matplotlib.projections.polar.PolarAxes.set_thetamax` +# +# .. tags:: +# +# plot-style: polar +# plot-style: scatter +# level: beginner diff --git a/galleries/examples/pyplots/axline.py b/galleries/examples/pyplots/axline.py index dde94af2fcdf..71c9994072a1 100644 --- a/galleries/examples/pyplots/axline.py +++ b/galleries/examples/pyplots/axline.py @@ -51,3 +51,9 @@ # - `matplotlib.axes.Axes.axhline` / `matplotlib.pyplot.axhline` # - `matplotlib.axes.Axes.axvline` / `matplotlib.pyplot.axvline` # - `matplotlib.axes.Axes.axline` / `matplotlib.pyplot.axline` +# +# +# .. seealso:: +# +# `~.Axes.axhspan`, `~.Axes.axvspan` draw rectangles that span the Axes in one +# direction and are bounded in the other direction. diff --git a/galleries/examples/scales/aspect_loglog.py b/galleries/examples/scales/aspect_loglog.py index 90c0422ca389..420721b9b411 100644 --- a/galleries/examples/scales/aspect_loglog.py +++ b/galleries/examples/scales/aspect_loglog.py @@ -1,6 +1,6 @@ """ ============= -Loglog Aspect +Loglog aspect ============= """ diff --git a/galleries/examples/shapes_and_collections/line_collection.py b/galleries/examples/shapes_and_collections/line_collection.py index a27496f62e0e..d8b3fd655133 100644 --- a/galleries/examples/shapes_and_collections/line_collection.py +++ b/galleries/examples/shapes_and_collections/line_collection.py @@ -1,7 +1,7 @@ """ -============================================= -Plotting multiple lines with a LineCollection -============================================= +========================================== +Plot multiple lines using a LineCollection +========================================== Matplotlib can efficiently draw multiple lines at once using a `~.LineCollection`. """ diff --git a/galleries/examples/shapes_and_collections/quad_bezier.py b/galleries/examples/shapes_and_collections/quad_bezier.py index 6f91ad85bf8f..f4a688233ba9 100644 --- a/galleries/examples/shapes_and_collections/quad_bezier.py +++ b/galleries/examples/shapes_and_collections/quad_bezier.py @@ -1,6 +1,6 @@ """ ============ -Bezier Curve +Bezier curve ============ This example showcases the `~.patches.PathPatch` object to create a Bezier diff --git a/galleries/examples/showcase/stock_prices.py b/galleries/examples/showcase/stock_prices.py index a3ec7ad2a252..bc372fb1211a 100644 --- a/galleries/examples/showcase/stock_prices.py +++ b/galleries/examples/showcase/stock_prices.py @@ -42,7 +42,7 @@ 'ADBE', 'GSPC', 'IXIC'] # Manually adjust the label positions vertically (units are points = 1/72 inch) -y_offsets = {k: 0 for k in stocks_ticker} +y_offsets = dict.fromkeys(stocks_ticker, 0) y_offsets['IBM'] = 5 y_offsets['AAPL'] = -5 y_offsets['AMZN'] = -6 diff --git a/galleries/examples/statistics/barchart_demo.py b/galleries/examples/statistics/barchart_demo.py deleted file mode 100644 index ad33f442b844..000000000000 --- a/galleries/examples/statistics/barchart_demo.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -=================================== -Percentiles as horizontal bar chart -=================================== - -Bar charts are useful for visualizing counts, or summary statistics -with error bars. Also see the :doc:`/gallery/lines_bars_and_markers/barchart` -or the :doc:`/gallery/lines_bars_and_markers/barh` example for simpler versions -of those features. - -This example comes from an application in which grade school gym -teachers wanted to be able to show parents how their child did across -a handful of fitness tests, and importantly, relative to how other -children did. To extract the plotting code for demo purposes, we'll -just make up some data for little Johnny Doe. -""" - -from collections import namedtuple - -import matplotlib.pyplot as plt -import numpy as np - -Student = namedtuple('Student', ['name', 'grade', 'gender']) -Score = namedtuple('Score', ['value', 'unit', 'percentile']) - - -def to_ordinal(num): - """Convert an integer to an ordinal string, e.g. 2 -> '2nd'.""" - suffixes = {str(i): v - for i, v in enumerate(['th', 'st', 'nd', 'rd', 'th', - 'th', 'th', 'th', 'th', 'th'])} - v = str(num) - # special case early teens - if v in {'11', '12', '13'}: - return v + 'th' - return v + suffixes[v[-1]] - - -def format_score(score): - """ - Create score labels for the right y-axis as the test name followed by the - measurement unit (if any), split over two lines. - """ - return f'{score.value}\n{score.unit}' if score.unit else str(score.value) - - -def plot_student_results(student, scores_by_test, cohort_size): - fig, ax1 = plt.subplots(figsize=(9, 7), layout='constrained') - fig.canvas.manager.set_window_title('Eldorado K-8 Fitness Chart') - - ax1.set_title(student.name) - ax1.set_xlabel( - 'Percentile Ranking Across {grade} Grade {gender}s\n' - 'Cohort Size: {cohort_size}'.format( - grade=to_ordinal(student.grade), - gender=student.gender.title(), - cohort_size=cohort_size)) - - test_names = list(scores_by_test.keys()) - percentiles = [score.percentile for score in scores_by_test.values()] - - rects = ax1.barh(test_names, percentiles, align='center', height=0.5) - # Partition the percentile values to be able to draw large numbers in - # white within the bar, and small numbers in black outside the bar. - large_percentiles = [to_ordinal(p) if p > 40 else '' for p in percentiles] - small_percentiles = [to_ordinal(p) if p <= 40 else '' for p in percentiles] - ax1.bar_label(rects, small_percentiles, - padding=5, color='black', fontweight='bold') - ax1.bar_label(rects, large_percentiles, - padding=-32, color='white', fontweight='bold') - - ax1.set_xlim([0, 100]) - ax1.set_xticks([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) - ax1.xaxis.grid(True, linestyle='--', which='major', - color='grey', alpha=.25) - ax1.axvline(50, color='grey', alpha=0.25) # median position - - # Set the right-hand Y-axis ticks and labels - ax2 = ax1.twinx() - # Set equal limits on both yaxis so that the ticks line up - ax2.set_ylim(ax1.get_ylim()) - # Set the tick locations and labels - ax2.set_yticks( - np.arange(len(scores_by_test)), - labels=[format_score(score) for score in scores_by_test.values()]) - - ax2.set_ylabel('Test Scores') - - -student = Student(name='Johnny Doe', grade=2, gender='Boy') -scores_by_test = { - 'Pacer Test': Score(7, 'laps', percentile=37), - 'Flexed Arm\n Hang': Score(48, 'sec', percentile=95), - 'Mile Run': Score('12:52', 'min:sec', percentile=73), - 'Agility': Score(17, 'sec', percentile=60), - 'Push Ups': Score(14, '', percentile=16), -} - -plot_student_results(student, scores_by_test, cohort_size=62) -plt.show() - -# %% -# -# .. admonition:: References -# -# 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.bar_label` / `matplotlib.pyplot.bar_label` -# - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx` diff --git a/galleries/examples/statistics/boxplot_demo.py b/galleries/examples/statistics/boxplot_demo.py index ec23408c0bfc..b642d7e9f658 100644 --- a/galleries/examples/statistics/boxplot_demo.py +++ b/galleries/examples/statistics/boxplot_demo.py @@ -34,23 +34,23 @@ axs[0, 0].set_title('basic plot') # notched plot -axs[0, 1].boxplot(data, 1) +axs[0, 1].boxplot(data, notch=True) axs[0, 1].set_title('notched plot') # change outlier point symbols -axs[0, 2].boxplot(data, 0, 'gD') +axs[0, 2].boxplot(data, sym='gD') axs[0, 2].set_title('change outlier\npoint symbols') # don't show outlier points -axs[1, 0].boxplot(data, 0, '') +axs[1, 0].boxplot(data, sym='') axs[1, 0].set_title("don't show\noutlier points") # horizontal boxes -axs[1, 1].boxplot(data, 0, 'rs', 0) +axs[1, 1].boxplot(data, sym='rs', orientation='horizontal') axs[1, 1].set_title('horizontal boxes') # change whisker length -axs[1, 2].boxplot(data, 0, 'rs', 0, 0.75) +axs[1, 2].boxplot(data, sym='rs', orientation='horizontal', whis=0.75) axs[1, 2].set_title('change whisker length') fig.subplots_adjust(left=0.08, right=0.98, bottom=0.05, top=0.9, @@ -107,7 +107,7 @@ fig.canvas.manager.set_window_title('A Boxplot Example') fig.subplots_adjust(left=0.075, right=0.95, top=0.9, bottom=0.25) -bp = ax1.boxplot(data, notch=False, sym='+', vert=True, whis=1.5) +bp = ax1.boxplot(data, notch=False, sym='+', orientation='vertical', whis=1.5) plt.setp(bp['boxes'], color='black') plt.setp(bp['whiskers'], color='black') plt.setp(bp['fliers'], color='red', marker='+') diff --git a/galleries/examples/statistics/errorbars_and_boxes.py b/galleries/examples/statistics/errorbars_and_boxes.py index b45eee751137..10face31f2d6 100644 --- a/galleries/examples/statistics/errorbars_and_boxes.py +++ b/galleries/examples/statistics/errorbars_and_boxes.py @@ -1,7 +1,7 @@ """ -==================================================== -Creating boxes from error bars using PatchCollection -==================================================== +================================================== +Create boxes from error bars using PatchCollection +================================================== In this example, we snazz up a pretty standard error bar plot by adding a rectangle patch defined by the limits of the bars in both the x- and diff --git a/galleries/examples/statistics/histogram_cumulative.py b/galleries/examples/statistics/histogram_cumulative.py index d87305629f8d..7d6735d7b9a6 100644 --- a/galleries/examples/statistics/histogram_cumulative.py +++ b/galleries/examples/statistics/histogram_cumulative.py @@ -1,7 +1,7 @@ """ -================================= -Plotting cumulative distributions -================================= +======================== +Cumulative distributions +======================== This example shows how to plot the empirical cumulative distribution function (ECDF) of a sample. We also show the theoretical CDF. diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index 78ff03719057..a85ec2acfa8d 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -14,8 +14,11 @@ shape of a histogram. The Astropy docs have a great section on how to select these parameters: http://docs.astropy.org/en/stable/visualization/histogram.html -""" +.. redirect-from:: /gallery/lines_bars_and_markers/filled_step + +""" +# %% import matplotlib.pyplot as plt import numpy as np @@ -45,6 +48,94 @@ fig.tight_layout() plt.show() +# %% +# ----------------------------------- +# Setting properties for each dataset +# ----------------------------------- +# +# You can style the histograms individually by passing a list of values to the +# following parameters: +# +# * edgecolor +# * facecolor +# * hatch +# * linewidth +# * linestyle +# +# +# edgecolor +# ......... + +fig, ax = plt.subplots() + +edgecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, fill=False, histtype="step", stacked=True, + edgecolor=edgecolors, label=edgecolors) +ax.legend() +ax.set_title('Stacked Steps with Edgecolors') + +plt.show() + +# %% +# facecolor +# ......... + +fig, ax = plt.subplots() + +facecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, histtype="barstacked", facecolor=facecolors, label=facecolors) +ax.legend() +ax.set_title("Bars with different Facecolors") + +plt.show() + +# %% +# hatch +# ..... + +fig, ax = plt.subplots() + +hatches = [".", "o", "x"] + +ax.hist(x, n_bins, histtype="barstacked", hatch=hatches, label=hatches) +ax.legend() +ax.set_title("Hatches on Stacked Bars") + +plt.show() + +# %% +# linewidth +# ......... + +fig, ax = plt.subplots() + +linewidths = [1, 2, 3] +edgecolors = ["green", "red", "blue"] + +ax.hist(x, n_bins, fill=False, histtype="bar", linewidth=linewidths, + edgecolor=edgecolors, label=linewidths) +ax.legend() +ax.set_title("Bars with Linewidths") + +plt.show() + +# %% +# linestyle +# ......... + +fig, ax = plt.subplots() + +linestyles = ['-', ':', '--'] + +ax.hist(x, n_bins, fill=False, histtype='bar', linestyle=linestyles, + edgecolor=edgecolors, label=linestyles) +ax.legend() +ax.set_title('Bars with Linestyles') + +plt.show() + # %% # # .. tags:: plot-type: histogram, domain: statistics, purpose: reference diff --git a/galleries/examples/statistics/multiple_histograms_side_by_side.py b/galleries/examples/statistics/multiple_histograms_side_by_side.py index 733aed51f253..684faa62a904 100644 --- a/galleries/examples/statistics/multiple_histograms_side_by_side.py +++ b/galleries/examples/statistics/multiple_histograms_side_by_side.py @@ -1,7 +1,7 @@ """ -========================================== -Producing multiple histograms side by side -========================================== +================================ +Multiple histograms side by side +================================ This example plots horizontal histograms of different samples along a categorical x-axis. Additionally, the histograms are plotted to diff --git a/galleries/examples/statistics/violinplot.py b/galleries/examples/statistics/violinplot.py index 3d0d44538032..7f4725ff7a8c 100644 --- a/galleries/examples/statistics/violinplot.py +++ b/galleries/examples/statistics/violinplot.py @@ -62,37 +62,37 @@ quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='high') axs[0, 5].set_title('Custom violin 6', fontsize=fs) -axs[1, 0].violinplot(data, pos, points=80, vert=False, widths=0.7, +axs[1, 0].violinplot(data, pos, points=80, orientation='horizontal', widths=0.7, showmeans=True, showextrema=True, showmedians=True) axs[1, 0].set_title('Custom violin 7', fontsize=fs) -axs[1, 1].violinplot(data, pos, points=100, vert=False, widths=0.9, +axs[1, 1].violinplot(data, pos, points=100, orientation='horizontal', widths=0.9, showmeans=True, showextrema=True, showmedians=True, bw_method='silverman') axs[1, 1].set_title('Custom violin 8', fontsize=fs) -axs[1, 2].violinplot(data, pos, points=200, vert=False, widths=1.1, +axs[1, 2].violinplot(data, pos, points=200, orientation='horizontal', widths=1.1, showmeans=True, showextrema=True, showmedians=True, bw_method=0.5) axs[1, 2].set_title('Custom violin 9', fontsize=fs) -axs[1, 3].violinplot(data, pos, points=200, vert=False, widths=1.1, +axs[1, 3].violinplot(data, pos, points=200, orientation='horizontal', widths=1.1, showmeans=True, showextrema=True, showmedians=True, quantiles=[[0.1], [], [], [0.175, 0.954], [0.75], [0.25]], bw_method=0.5) axs[1, 3].set_title('Custom violin 10', fontsize=fs) -axs[1, 4].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1, - showmeans=True, showextrema=True, showmedians=True, +axs[1, 4].violinplot(data[-1:], pos[-1:], points=200, orientation='horizontal', + widths=1.1, showmeans=True, showextrema=True, showmedians=True, quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5) axs[1, 4].set_title('Custom violin 11', fontsize=fs) -axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1, - showmeans=True, showextrema=True, showmedians=True, +axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, orientation='horizontal', + widths=1.1, showmeans=True, showextrema=True, showmedians=True, quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='low') -axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1, - showmeans=True, showextrema=True, showmedians=True, +axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, orientation='horizontal', + widths=1.1, showmeans=True, showextrema=True, showmedians=True, quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='high') axs[1, 5].set_title('Custom violin 12', fontsize=fs) diff --git a/galleries/examples/style_sheets/petroff10.py b/galleries/examples/style_sheets/petroff10.py new file mode 100644 index 000000000000..f6293fd40a6b --- /dev/null +++ b/galleries/examples/style_sheets/petroff10.py @@ -0,0 +1,43 @@ +""" +===================== +Petroff10 style sheet +===================== + +This example demonstrates the "petroff10" style, which implements the 10-color +sequence developed by Matthew A. Petroff [1]_ for accessible data visualization. +The style balances aesthetics with accessibility considerations, making it +suitable for various types of plots while ensuring readability and distinction +between data series. + +.. [1] https://arxiv.org/abs/2107.02270 + +""" + +import matplotlib.pyplot as plt +import numpy as np + + +def colored_lines_example(ax): + t = np.linspace(-10, 10, 100) + nb_colors = len(plt.rcParams['axes.prop_cycle']) + shifts = np.linspace(-5, 5, nb_colors) + amplitudes = np.linspace(1, 1.5, nb_colors) + for t0, a in zip(shifts, amplitudes): + y = a / (1 + np.exp(-(t - t0))) + line, = ax.plot(t, y, '-') + point_indices = np.linspace(0, len(t) - 1, 20, dtype=int) + ax.plot(t[point_indices], y[point_indices], 'o', color=line.get_color()) + ax.set_xlim(-10, 10) + + +def image_and_patch_example(ax): + ax.imshow(np.random.random(size=(20, 20)), interpolation='none') + c = plt.Circle((5, 5), radius=5, label='patch') + ax.add_patch(c) + +plt.style.use('petroff10') +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12, 5)) +fig.suptitle("'petroff10' style sheet") +colored_lines_example(ax1) +image_and_patch_example(ax2) +plt.show() diff --git a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py index 4935878ee027..abb048ba395c 100644 --- a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py @@ -1,7 +1,7 @@ """ -========================== -Aligning Labels and Titles -========================== +======================= +Align labels and titles +======================= Aligning xlabel, ylabel, and title using `.Figure.align_xlabels`, `.Figure.align_ylabels`, and `.Figure.align_titles`. @@ -41,3 +41,11 @@ fig.align_titles() plt.show() + +# %% +# .. tags:: +# +# component: label +# component: title +# styling: position +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py index e0a8c76a0e61..ec865798d648 100644 --- a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py +++ b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py @@ -1,7 +1,7 @@ """ -=============================================== -Programmatically controlling subplot adjustment -=============================================== +=========================================== +Programmatically control subplot adjustment +=========================================== .. note:: @@ -85,3 +85,10 @@ def on_draw(event): # - `matplotlib.figure.Figure.subplots_adjust` # - `matplotlib.gridspec.SubplotParams` # - `matplotlib.backend_bases.FigureCanvasBase.mpl_connect` +# +# .. tags:: +# +# component: subplot +# plot-type: line +# styling: position +# level: advanced diff --git a/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py b/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py index 74b64f72c466..e17f21e7d41b 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py @@ -154,3 +154,9 @@ # in this example: # # - `matplotlib.axes.Axes.set_box_aspect` +# +# .. tags:: +# +# component: axes +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_demo.py b/galleries/examples/subplots_axes_and_figures/axes_demo.py index f5620a9a980d..07f3ca2070c2 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axes_demo.py @@ -43,3 +43,11 @@ left_inset_ax.set(title='Impulse response', xlim=(0, .2), xticks=[], yticks=[]) plt.show() + +# %% +# .. tags:: +# +# component: axes +# plot-type: line +# plot-type: histogram +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_margins.py b/galleries/examples/subplots_axes_and_figures/axes_margins.py index dd113c8c34e0..30298168c8e8 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_margins.py +++ b/galleries/examples/subplots_axes_and_figures/axes_margins.py @@ -86,3 +86,11 @@ def f(t): # - `matplotlib.axes.Axes.use_sticky_edges` # - `matplotlib.axes.Axes.pcolor` / `matplotlib.pyplot.pcolor` # - `matplotlib.patches.Polygon` +# +# .. tags:: +# +# component: axes +# plot-type: line +# plot-type: imshow +# plot-type: pcolor +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_props.py b/galleries/examples/subplots_axes_and_figures/axes_props.py index f2e52febed34..6bbcc88ad5b8 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_props.py +++ b/galleries/examples/subplots_axes_and_figures/axes_props.py @@ -1,7 +1,7 @@ """ -========== -Axes Props -========== +=============== +Axes properties +=============== You can control the axis tick and grid properties """ @@ -19,3 +19,10 @@ ax.tick_params(labelcolor='r', labelsize='medium', width=3) plt.show() + +# %% +# .. tags:: +# +# component: ticks +# plot-type: line +# level: beginner 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 49a44b9e4f43..c8d09de45888 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py @@ -1,6 +1,6 @@ """ ================ -Axes Zoom Effect +Axes zoom effect ================ """ @@ -120,3 +120,10 @@ def zoom_effect02(ax1, ax2, **kwargs): zoom_effect02(axs["zoom2"], axs["main"]) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# component: transform +# level: advanced diff --git a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py index e297f4adf462..971c6002ee71 100644 --- a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py @@ -1,36 +1,55 @@ """ -============ -axhspan Demo -============ +============================== +Draw regions that span an Axes +============================== -Create lines or rectangles that span the Axes in either the horizontal or -vertical direction, and lines than span the Axes with an arbitrary orientation. +`~.Axes.axhspan` and `~.Axes.axvspan` draw rectangles that span the Axes in either +the horizontal or vertical direction and are bounded in the other direction. They are +often used to highlight data regions. """ import matplotlib.pyplot as plt import numpy as np -t = np.arange(-1, 2, .01) -s = np.sin(2 * np.pi * t) - -fig, ax = plt.subplots() - -ax.plot(t, s) -# Thick red horizontal line at y=0 that spans the xrange. -ax.axhline(linewidth=8, color='#d62728') -# Horizontal line at y=1 that spans the xrange. -ax.axhline(y=1) -# Vertical line at x=1 that spans the yrange. -ax.axvline(x=1) -# Thick blue vertical line at x=0 that spans the upper quadrant of the yrange. -ax.axvline(x=0, ymin=0.75, linewidth=8, color='#1f77b4') -# Default hline at y=.5 that spans the middle half of the Axes. -ax.axhline(y=.5, xmin=0.25, xmax=0.75) -# Infinite black line going through (0, 0) to (1, 1). -ax.axline((0, 0), (1, 1), color='k') -# 50%-gray rectangle spanning the Axes' width from y=0.25 to y=0.75. -ax.axhspan(0.25, 0.75, facecolor='0.5') -# Green rectangle spanning the Axes' height from x=1.25 to x=1.55. -ax.axvspan(1.25, 1.55, facecolor='#2ca02c') +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3)) + +np.random.seed(19680801) +s = 2.9 * np.convolve(np.random.randn(500), np.ones(30) / 30, mode='valid') +ax1.plot(s) +ax1.axhspan(-1, 1, alpha=0.1) +ax1.set(ylim=(-1.5, 1.5), title="axhspan") + + +mu = 8 +sigma = 2 +x = np.linspace(0, 16, 401) +y = np.exp(-((x-mu)**2)/(2*sigma**2)) +ax2.axvspan(mu-2*sigma, mu-sigma, color='0.95') +ax2.axvspan(mu-sigma, mu+sigma, color='0.9') +ax2.axvspan(mu+sigma, mu+2*sigma, color='0.95') +ax2.axvline(mu, color='darkgrey', linestyle='--') +ax2.plot(x, y) +ax2.set(title="axvspan") plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.axes.Axes.axhspan` / `matplotlib.pyplot.axhspan` +# - `matplotlib.axes.Axes.axvspan` / `matplotlib.pyplot.axvspan` +# +# +# .. seealso:: +# +# `~.Axes.axhline`, `~.Axes.axvline`, `~.Axes.axline` draw infinite lines. +# +# .. tags:: +# +# styling: shape +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py b/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py index 6ac4d66da0e8..046af386ae59 100644 --- a/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py @@ -33,3 +33,11 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: axes +# styling: size +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py index 8b9d38240e42..2d0bc427b1f9 100644 --- a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py @@ -1,6 +1,6 @@ """ =================== -Axis Label Position +Axis label position =================== Choose axis label position when calling `~.Axes.set_xlabel` and @@ -18,3 +18,10 @@ cbar.set_label("ZLabel", loc='top') plt.show() + +# %% +# .. tags:: +# +# component: axis +# styling: position +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/broken_axis.py b/galleries/examples/subplots_axes_and_figures/broken_axis.py index 06263b9c120a..6305e613e327 100644 --- a/galleries/examples/subplots_axes_and_figures/broken_axis.py +++ b/galleries/examples/subplots_axes_and_figures/broken_axis.py @@ -1,6 +1,6 @@ """ =========== -Broken Axis +Broken axis =========== Broken axis example, where the y-axis will have a portion cut out. @@ -52,3 +52,10 @@ plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/custom_figure_class.py b/galleries/examples/subplots_axes_and_figures/custom_figure_class.py index 96c7f1113787..328447062a5b 100644 --- a/galleries/examples/subplots_axes_and_figures/custom_figure_class.py +++ b/galleries/examples/subplots_axes_and_figures/custom_figure_class.py @@ -50,3 +50,10 @@ def __init__(self, *args, watermark=None, **kwargs): # - `matplotlib.pyplot.figure` # - `matplotlib.figure.Figure` # - `matplotlib.figure.Figure.text` +# +# .. tags:: +# +# component: figure +# plot-type: line +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py index 9a67541e554e..b3a59ce048c0 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py @@ -1,7 +1,7 @@ """ -===================================== -Resizing Axes with constrained layout -===================================== +=================================== +Resize Axes with constrained layout +=================================== *Constrained layout* attempts to resize subplots in a figure so that there are no overlaps between Axes objects and labels @@ -69,3 +69,10 @@ def example_plot(ax): # # - `matplotlib.gridspec.GridSpec` # - `matplotlib.gridspec.GridSpecFromSubplotSpec` +# +# .. tags:: +# +# component: axes +# component: subplot +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py index 7ac3a7376d67..4ac0f1b99dfc 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py @@ -1,7 +1,7 @@ """ -=============================== -Resizing Axes with tight layout -=============================== +============================= +Resize Axes with tight layout +============================= `~.Figure.tight_layout` attempts to resize subplots in a figure so that there are no overlaps between Axes objects and labels on the Axes. @@ -132,3 +132,10 @@ def example_plot(ax): # - `matplotlib.figure.Figure.add_gridspec` # - `matplotlib.figure.Figure.add_subplot` # - `matplotlib.pyplot.subplot2grid` +# +# .. tags:: +# +# component: axes +# component: subplot +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py b/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py index 216641657b06..95b92482d5ac 100644 --- a/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py +++ b/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py @@ -44,3 +44,10 @@ def convert_ax_c_to_celsius(ax_f): plt.show() make_plot() + +# %% +# .. tags:: +# +# component: axes +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/figure_size_units.py b/galleries/examples/subplots_axes_and_figures/figure_size_units.py index 0ce49c937d50..50292ef92b74 100644 --- a/galleries/examples/subplots_axes_and_figures/figure_size_units.py +++ b/galleries/examples/subplots_axes_and_figures/figure_size_units.py @@ -79,3 +79,9 @@ # - `matplotlib.pyplot.figure` # - `matplotlib.pyplot.subplots` # - `matplotlib.pyplot.subplot_mosaic` +# +# .. tags:: +# +# component: figure +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/figure_title.py b/galleries/examples/subplots_axes_and_figures/figure_title.py index 85e5044c4eba..1b0eb1a00b23 100644 --- a/galleries/examples/subplots_axes_and_figures/figure_title.py +++ b/galleries/examples/subplots_axes_and_figures/figure_title.py @@ -51,3 +51,11 @@ fig.supylabel('Stock price relative to max') plt.show() + +# %% +# .. tags:: +# +# component: figure +# component: title +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/ganged_plots.py b/galleries/examples/subplots_axes_and_figures/ganged_plots.py index e25bb16a15e5..3229d64a15b4 100644 --- a/galleries/examples/subplots_axes_and_figures/ganged_plots.py +++ b/galleries/examples/subplots_axes_and_figures/ganged_plots.py @@ -1,7 +1,7 @@ """ -========================== -Creating adjacent subplots -========================== +================= +Adjacent subplots +================= To create plots that share a common axis (visually) you can set the hspace between the subplots to zero. Passing sharex=True when creating the subplots @@ -38,3 +38,10 @@ axs[2].set_ylim(-1, 1) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/geo_demo.py b/galleries/examples/subplots_axes_and_figures/geo_demo.py index f2f22751b215..256c440cc4d1 100644 --- a/galleries/examples/subplots_axes_and_figures/geo_demo.py +++ b/galleries/examples/subplots_axes_and_figures/geo_demo.py @@ -40,3 +40,10 @@ plt.grid(True) plt.show() + +# %% +# .. tags:: +# +# plot-type: specialty +# component: projection +# domain: cartography diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py index 0535a7afdde4..9996bde9306a 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py @@ -1,7 +1,7 @@ """ -================================================== -Combining two subplots using subplots and GridSpec -================================================== +================================================ +Combine two subplots using subplots and GridSpec +================================================ Sometimes we want to combine two subplots in an Axes layout created with `~.Figure.subplots`. We can get the `~.gridspec.GridSpec` from the Axes @@ -28,3 +28,9 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py index 54c3d8fa63cc..3762dec4fdb8 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py @@ -1,7 +1,7 @@ """ -======================================================= -Using Gridspec to make multi-column/row subplot layouts -======================================================= +============================================= +Gridspec for multi-column/row subplot layouts +============================================= `.GridSpec` is a flexible way to layout subplot grids. Here is an example with a 3x3 grid, and @@ -32,3 +32,9 @@ def format_axes(fig): format_axes(fig) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_nested.py b/galleries/examples/subplots_axes_and_figures/gridspec_nested.py index bfcb90cdfc4a..025bdb1185a7 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_nested.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_nested.py @@ -44,3 +44,9 @@ def format_axes(fig): format_axes(fig) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/invert_axes.py b/galleries/examples/subplots_axes_and_figures/invert_axes.py index 31f4d75680ce..40a4ca2479b7 100644 --- a/galleries/examples/subplots_axes_and_figures/invert_axes.py +++ b/galleries/examples/subplots_axes_and_figures/invert_axes.py @@ -33,3 +33,10 @@ ax2.grid(True) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py b/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py index d6b6a5ed48c6..fe3b2ab191a1 100644 --- a/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py +++ b/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py @@ -1,7 +1,7 @@ """ -=================================== -Managing multiple figures in pyplot -=================================== +================================= +Manage multiple figures in pyplot +================================= `matplotlib.pyplot` uses the concept of a *current figure* and *current Axes*. Figures are identified via a figure number that is passed to `~.pyplot.figure`. diff --git a/galleries/examples/subplots_axes_and_figures/secondary_axis.py b/galleries/examples/subplots_axes_and_figures/secondary_axis.py index ab42d3a6c182..842b296f78cf 100644 --- a/galleries/examples/subplots_axes_and_figures/secondary_axis.py +++ b/galleries/examples/subplots_axes_and_figures/secondary_axis.py @@ -17,7 +17,6 @@ import numpy as np import matplotlib.dates as mdates -from matplotlib.ticker import AutoMinorLocator fig, ax = plt.subplots(layout='constrained') x = np.arange(0, 360, 1) @@ -96,48 +95,47 @@ def one_over(x): plt.show() # %% -# Sometime we want to relate the axes in a transform that is ad-hoc from -# the data, and is derived empirically. In that case we can set the -# forward and inverse transforms functions to be linear interpolations from the -# one data set to the other. +# Sometime we want to relate the axes in a transform that is ad-hoc from the data, and +# is derived empirically. Or, one axis could be a complicated nonlinear function of the +# other. In these cases we can set the forward and inverse transform functions to be +# linear interpolations from the one set of independent variables to the other. # # .. note:: # # In order to properly handle the data margins, the mapping functions # (``forward`` and ``inverse`` in this example) need to be defined beyond the -# nominal plot limits. -# -# In the specific case of the numpy linear interpolation, `numpy.interp`, -# this condition can be arbitrarily enforced by providing optional keyword -# arguments *left*, *right* such that values outside the data range are -# mapped well outside the plot limits. +# nominal plot limits. This condition can be enforced by extending the +# interpolation beyond the plotted values, both to the left and the right, +# see ``x1n`` and ``x2n`` below. fig, ax = plt.subplots(layout='constrained') -xdata = np.arange(1, 11, 0.4) -ydata = np.random.randn(len(xdata)) -ax.plot(xdata, ydata, label='Plotted data') - -xold = np.arange(0, 11, 0.2) -# fake data set relating x coordinate to another data-derived coordinate. -# xnew must be monotonic, so we sort... -xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3) - -ax.plot(xold[3:], xnew[3:], label='Transform data') -ax.set_xlabel('X [m]') +x1_vals = np.arange(2, 11, 0.4) +# second independent variable is a nonlinear function of the other. +x2_vals = x1_vals ** 2 +ydata = 50.0 + 20 * np.random.randn(len(x1_vals)) +ax.plot(x1_vals, ydata, label='Plotted data') +ax.plot(x1_vals, x2_vals, label=r'$x_2 = x_1^2$') +ax.set_xlabel(r'$x_1$') ax.legend() +# the forward and inverse functions must be defined on the complete visible axis range +x1n = np.linspace(0, 20, 201) +x2n = x1n**2 + def forward(x): - return np.interp(x, xold, xnew) + return np.interp(x, x1n, x2n) def inverse(x): - return np.interp(x, xnew, xold) - + return np.interp(x, x2n, x1n) +# use axvline to prove that the derived secondary axis is correctly plotted +ax.axvline(np.sqrt(40), color="grey", ls="--") +ax.axvline(10, color="grey", ls="--") secax = ax.secondary_xaxis('top', functions=(forward, inverse)) -secax.xaxis.set_minor_locator(AutoMinorLocator()) -secax.set_xlabel('$X_{other}$') +secax.set_xticks([10, 20, 40, 60, 80, 100]) +secax.set_xlabel(r'$x_2$') plt.show() @@ -211,3 +209,9 @@ def anomaly_to_celsius(x): # # - `matplotlib.axes.Axes.secondary_xaxis` # - `matplotlib.axes.Axes.secondary_yaxis` +# +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py index 234a15660f2d..e0aa04d13def 100644 --- a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py +++ b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py @@ -1,6 +1,7 @@ """ -Sharing axis limits and views -============================= +=========================== +Share axis limits and views +=========================== It's common to make two or more plots which share an axis, e.g., two subplots with time as a common axis. When you pan and zoom around on one, you want the @@ -22,3 +23,10 @@ ax2.plot(t, np.sin(4*np.pi*t)) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py index 6b3b3839a437..a5c000a24a96 100644 --- a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py +++ b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py @@ -55,3 +55,10 @@ plt.plot(t, s3) plt.xlim(0.01, 5.0) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subfigures.py b/galleries/examples/subplots_axes_and_figures/subfigures.py index 6272de975c4d..cbe62f57d6b1 100644 --- a/galleries/examples/subplots_axes_and_figures/subfigures.py +++ b/galleries/examples/subplots_axes_and_figures/subfigures.py @@ -13,9 +13,6 @@ `matplotlib.figure.Figure.subfigures` to make an array of subfigures. Note that subfigures can also have their own child subfigures. -.. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. - """ import matplotlib.pyplot as plt import numpy as np @@ -146,3 +143,10 @@ def example_plot(ax, fontsize=12, hide_labels=False): axsRight = subfigs[1].subplots(2, 2) plt.show() + +# %% +# .. tags:: +# +# component: figure +# plot-type: pcolormesh +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/subplot.py b/galleries/examples/subplots_axes_and_figures/subplot.py index 4b78e7a5a840..e23b86fa3e9c 100644 --- a/galleries/examples/subplots_axes_and_figures/subplot.py +++ b/galleries/examples/subplots_axes_and_figures/subplot.py @@ -49,3 +49,10 @@ plt.ylabel('Undamped') plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subplots_adjust.py b/galleries/examples/subplots_axes_and_figures/subplots_adjust.py index d4393be51fb4..8e3b876adfeb 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_adjust.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_adjust.py @@ -29,3 +29,10 @@ plt.colorbar(cax=cax) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: imshow +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 229ecd34cc9f..0e3cb1102230 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -1,7 +1,7 @@ """ -================================================= -Creating multiple subplots using ``plt.subplots`` -================================================= +=============================================== +Create multiple subplots using ``plt.subplots`` +=============================================== `.pyplot.subplots` creates a figure and a grid of subplots with a single call, while providing reasonable control over how the individual plots are created. @@ -209,3 +209,14 @@ ax2.plot(x, y ** 2) plt.show() + +# %% +# .. tags:: +# +# component: subplot, +# component: axes, +# component: axis +# plot-type: line, +# plot-type: polar +# level: beginner +# purpose: showcase diff --git a/galleries/examples/subplots_axes_and_figures/two_scales.py b/galleries/examples/subplots_axes_and_figures/two_scales.py index 249a65fd64fe..882fcac7866e 100644 --- a/galleries/examples/subplots_axes_and_figures/two_scales.py +++ b/galleries/examples/subplots_axes_and_figures/two_scales.py @@ -49,3 +49,9 @@ # - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx` # - `matplotlib.axes.Axes.twiny` / `matplotlib.pyplot.twiny` # - `matplotlib.axes.Axes.tick_params` / `matplotlib.pyplot.tick_params` +# +# .. tags:: +# +# component: axes +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py b/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py index 4cbd9875e4bc..4453b3ec39f1 100644 --- a/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py +++ b/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py @@ -43,3 +43,9 @@ # - `matplotlib.axes.Axes.inset_axes` # - `matplotlib.axes.Axes.indicate_inset_zoom` # - `matplotlib.axes.Axes.imshow` +# +# .. tags:: +# +# component: axes +# plot-type: imshow +# level: intermediate diff --git a/galleries/examples/text_labels_and_annotations/annotate_transform.py b/galleries/examples/text_labels_and_annotations/annotate_transform.py index b2ce1de6a0c1..e7d4e11d9d38 100644 --- a/galleries/examples/text_labels_and_annotations/annotate_transform.py +++ b/galleries/examples/text_labels_and_annotations/annotate_transform.py @@ -1,6 +1,6 @@ """ ================== -Annotate Transform +Annotate transform ================== This example shows how to use different coordinate systems for annotations. diff --git a/galleries/examples/text_labels_and_annotations/annotation_demo.py b/galleries/examples/text_labels_and_annotations/annotation_demo.py index 5358bfaac60a..562948bcc512 100644 --- a/galleries/examples/text_labels_and_annotations/annotation_demo.py +++ b/galleries/examples/text_labels_and_annotations/annotation_demo.py @@ -1,7 +1,7 @@ """ -================ -Annotating Plots -================ +============== +Annotate plots +============== The following examples show ways to annotate plots in Matplotlib. This includes highlighting specific points of interest and using various diff --git a/galleries/examples/text_labels_and_annotations/annotation_polar.py b/galleries/examples/text_labels_and_annotations/annotation_polar.py index bbd46478bced..c2418519cf8c 100644 --- a/galleries/examples/text_labels_and_annotations/annotation_polar.py +++ b/galleries/examples/text_labels_and_annotations/annotation_polar.py @@ -1,7 +1,7 @@ """ -================ -Annotation Polar -================ +==================== +Annotate polar plots +==================== This example shows how to create an annotation on a polar graph. diff --git a/galleries/examples/text_labels_and_annotations/autowrap.py b/galleries/examples/text_labels_and_annotations/autowrap.py index e52dc919ee1b..ea65b0be9992 100644 --- a/galleries/examples/text_labels_and_annotations/autowrap.py +++ b/galleries/examples/text_labels_and_annotations/autowrap.py @@ -1,10 +1,10 @@ """ -================== -Auto-wrapping text -================== +============== +Auto-wrap text +============== -Matplotlib can wrap text automatically, but if it's too long, the text will be -displayed slightly outside of the boundaries of the axis anyways. +Matplotlib can wrap text automatically, but if it's too long, the text will +still be displayed slightly outside the boundaries of the axis. Note: Auto-wrapping does not work together with ``savefig(..., bbox_inches='tight')``. The 'tight' setting rescales the canvas diff --git a/galleries/examples/text_labels_and_annotations/custom_legends.py b/galleries/examples/text_labels_and_annotations/custom_legends.py index 18ace0513228..80200c528224 100644 --- a/galleries/examples/text_labels_and_annotations/custom_legends.py +++ b/galleries/examples/text_labels_and_annotations/custom_legends.py @@ -1,7 +1,7 @@ """ -======================== -Composing Custom Legends -======================== +====================== +Compose custom legends +====================== Composing custom legends piece-by-piece. diff --git a/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py b/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py index f8f3a108629c..9cb7f30302fc 100644 --- a/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py +++ b/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py @@ -1,6 +1,6 @@ r""" ================== -Text Rotation Mode +Text rotation mode ================== This example illustrates the effect of ``rotation_mode`` on the positioning diff --git a/galleries/examples/text_labels_and_annotations/engineering_formatter.py b/galleries/examples/text_labels_and_annotations/engineering_formatter.py index 573552b11a26..372297a81d57 100644 --- a/galleries/examples/text_labels_and_annotations/engineering_formatter.py +++ b/galleries/examples/text_labels_and_annotations/engineering_formatter.py @@ -1,7 +1,7 @@ """ -========================================= -Labeling ticks using engineering notation -========================================= +======================================= +Format ticks using engineering notation +======================================= Use of the engineering Formatter. """ diff --git a/galleries/examples/text_labels_and_annotations/font_family_rc.py b/galleries/examples/text_labels_and_annotations/font_family_rc.py index b3433dc9cdf1..bdf993b76a9e 100644 --- a/galleries/examples/text_labels_and_annotations/font_family_rc.py +++ b/galleries/examples/text_labels_and_annotations/font_family_rc.py @@ -1,7 +1,7 @@ """ -=========================== -Configuring the font family -=========================== +========================= +Configure the font family +========================= You can explicitly set which font family is picked up, either by specifying family names of fonts installed on user's system, or generic-families diff --git a/galleries/examples/text_labels_and_annotations/mathtext_examples.py b/galleries/examples/text_labels_and_annotations/mathtext_examples.py index 0cdae3c8193c..f9f8e628e08b 100644 --- a/galleries/examples/text_labels_and_annotations/mathtext_examples.py +++ b/galleries/examples/text_labels_and_annotations/mathtext_examples.py @@ -1,7 +1,7 @@ """ -================= -Mathtext Examples -================= +======================== +Mathematical expressions +======================== Selected features of Matplotlib's math rendering engine. """ diff --git a/galleries/examples/text_labels_and_annotations/rainbow_text.py b/galleries/examples/text_labels_and_annotations/rainbow_text.py index 35cedb9bbd0b..4c14f8289cbc 100644 --- a/galleries/examples/text_labels_and_annotations/rainbow_text.py +++ b/galleries/examples/text_labels_and_annotations/rainbow_text.py @@ -1,7 +1,7 @@ """ -==================================================== -Concatenating text objects with different properties -==================================================== +================================================== +Concatenate text objects with different properties +================================================== The example strings together several Text objects with different properties (e.g., color or font), positioning each one after the other. The first Text diff --git a/galleries/examples/text_labels_and_annotations/tex_demo.py b/galleries/examples/text_labels_and_annotations/tex_demo.py index 5eba9a14c2b7..df040c5a866a 100644 --- a/galleries/examples/text_labels_and_annotations/tex_demo.py +++ b/galleries/examples/text_labels_and_annotations/tex_demo.py @@ -1,7 +1,7 @@ """ -================================== -Rendering math equations using TeX -================================== +=============================== +Render math equations using TeX +=============================== You can use TeX to render all of your Matplotlib text by setting :rc:`text.usetex` to True. This requires that you have TeX and the other diff --git a/galleries/examples/text_labels_and_annotations/text_commands.py b/galleries/examples/text_labels_and_annotations/text_commands.py index 35f2c1c1a0c4..0650ff53bd5d 100644 --- a/galleries/examples/text_labels_and_annotations/text_commands.py +++ b/galleries/examples/text_labels_and_annotations/text_commands.py @@ -1,7 +1,7 @@ """ -============= -Text Commands -============= +=============== +Text properties +=============== Plotting text of many different kinds. diff --git a/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py b/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py index 4672f5c5772d..ae29385e8a6d 100644 --- a/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py +++ b/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py @@ -1,7 +1,7 @@ """ -============================== -Text Rotation Relative To Line -============================== +======================================= +Text rotation angle in data coordinates +======================================= Text objects in matplotlib are normally rotated with respect to the screen coordinate system (i.e., 45 degrees rotation plots text along a diff --git a/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py b/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py index 49303e244821..e529b1c8b2de 100644 --- a/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py +++ b/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py @@ -1,6 +1,6 @@ """ ==================== -Usetex Baseline Test +Usetex text baseline ==================== Comparison of text baselines computed for mathtext and usetex. diff --git a/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py b/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py index a289f3854ed7..ba1c944536cb 100644 --- a/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py +++ b/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py @@ -1,7 +1,7 @@ """ -================== -Usetex Fonteffects -================== +=================== +Usetex font effects +=================== This script demonstrates that font effects specified in your pdftex.map are now supported in usetex mode. diff --git a/galleries/examples/ticks/centered_ticklabels.py b/galleries/examples/ticks/centered_ticklabels.py index ab9e1b56c4e6..c3ccd67b0f5c 100644 --- a/galleries/examples/ticks/centered_ticklabels.py +++ b/galleries/examples/ticks/centered_ticklabels.py @@ -1,7 +1,7 @@ """ -============================== -Centering labels between ticks -============================== +=========================== +Center labels between ticks +=========================== Ticklabels are aligned relative to their associated tick. The alignment 'center', 'left', or 'right' can be controlled using the horizontal alignment diff --git a/galleries/examples/ticks/date_concise_formatter.py b/galleries/examples/ticks/date_concise_formatter.py index 540ebf0e56c1..ce5372aa9547 100644 --- a/galleries/examples/ticks/date_concise_formatter.py +++ b/galleries/examples/ticks/date_concise_formatter.py @@ -1,9 +1,9 @@ """ .. _date_concise_formatter: -================================================ -Formatting date ticks using ConciseDateFormatter -================================================ +============================================ +Format date ticks using ConciseDateFormatter +============================================ Finding good tick values and formatting the ticks for an axis that has date data is often a challenge. `~.dates.ConciseDateFormatter` is diff --git a/galleries/examples/ticks/date_precision_and_epochs.py b/galleries/examples/ticks/date_precision_and_epochs.py index c4b87127d3c0..eb4926cab68d 100644 --- a/galleries/examples/ticks/date_precision_and_epochs.py +++ b/galleries/examples/ticks/date_precision_and_epochs.py @@ -1,6 +1,6 @@ """ ========================= -Date Precision and Epochs +Date precision and epochs ========================= Matplotlib can handle `.datetime` objects and `numpy.datetime64` objects using diff --git a/galleries/examples/ticks/engformatter_offset.py b/galleries/examples/ticks/engformatter_offset.py new file mode 100644 index 000000000000..7da2d45a7942 --- /dev/null +++ b/galleries/examples/ticks/engformatter_offset.py @@ -0,0 +1,33 @@ +""" +=================================================== +SI prefixed offsets and natural order of magnitudes +=================================================== + +`matplotlib.ticker.EngFormatter` is capable of computing a natural +offset for your axis data, and presenting it with a standard SI prefix +automatically calculated. + +Below is an examples of such a plot: + +""" + +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.ticker as mticker + +# Fixing random state for reproducibility +np.random.seed(19680801) + +UNIT = "Hz" + +fig, ax = plt.subplots() +ax.yaxis.set_major_formatter(mticker.EngFormatter( + useOffset=True, + unit=UNIT +)) +size = 100 +measurement = np.full(size, 1e9) +noise = np.random.uniform(low=-2e3, high=2e3, size=size) +ax.plot(measurement + noise) +plt.show() diff --git a/galleries/examples/ticks/ticklabels_rotation.py b/galleries/examples/ticks/ticklabels_rotation.py index 94924d0440f5..d337ca827cde 100644 --- a/galleries/examples/ticks/ticklabels_rotation.py +++ b/galleries/examples/ticks/ticklabels_rotation.py @@ -1,9 +1,7 @@ """ -=========================== -Rotating custom tick labels -=========================== - -Demo of custom tick-labels with user-defined rotation. +=================== +Rotated tick labels +=================== """ import matplotlib.pyplot as plt @@ -14,7 +12,22 @@ fig, ax = plt.subplots() ax.plot(x, y) -# You can specify a rotation for the tick labels in degrees or with keywords. +# A tick label rotation can be set using Axes.tick_params. +ax.tick_params("y", rotation=45) +# Alternatively, if setting custom labels with set_xticks/set_yticks, it can +# be set at the same time as the labels. +# For both APIs, the rotation can be an angle in degrees, or one of the strings +# "horizontal" or "vertical". ax.set_xticks(x, labels, rotation='vertical') plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.axes.Axes.tick_params` / `matplotlib.pyplot.tick_params` +# - `matplotlib.axes.Axes.set_xticks` / `matplotlib.pyplot.xticks` diff --git a/galleries/examples/units/basic_units.py b/galleries/examples/units/basic_units.py index f9a94bcf6e37..3f64d145b65e 100644 --- a/galleries/examples/units/basic_units.py +++ b/galleries/examples/units/basic_units.py @@ -2,11 +2,12 @@ .. _basic_units: =========== -Basic Units +Basic units =========== """ +import itertools import math from packaging.version import parse as parse_version @@ -254,7 +255,7 @@ def get_unit(self): class UnitResolver: def addition_rule(self, units): - for unit_1, unit_2 in zip(units[:-1], units[1:]): + for unit_1, unit_2 in itertools.pairwise(units): if unit_1 != unit_2: return NotImplemented return units[0] diff --git a/galleries/examples/units/units_sample.py b/galleries/examples/units/units_sample.py index 5c1d53fa2dee..2690ee7db727 100644 --- a/galleries/examples/units/units_sample.py +++ b/galleries/examples/units/units_sample.py @@ -1,6 +1,6 @@ """ ====================== -Inches and Centimeters +Inches and centimeters ====================== The example illustrates the ability to override default x and y units (ax1) to 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 f6892a849a88..7c3b04041009 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -1,7 +1,7 @@ """ -=========================================== -Embedding in GTK3 with a navigation toolbar -=========================================== +======================================= +Embed in GTK3 with a navigation toolbar +======================================= Demonstrate NavigationToolbar with GTK3 accessed via pygobject. """ @@ -22,7 +22,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK3") +win.set_title("Embedded in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py index 170a88a58aff..51ceebb501e3 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -1,7 +1,7 @@ """ -================= -Embedding in GTK3 -================= +============= +Embed in GTK3 +============= Demonstrate adding a FigureCanvasGTK3Agg widget to a Gtk.ScrolledWindow using GTK3 accessed via pygobject. @@ -21,7 +21,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK3") +win.set_title("Embedded in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() 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 3e8568091236..e42e59459198 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -1,7 +1,7 @@ """ -=========================================== -Embedding in GTK4 with a navigation toolbar -=========================================== +======================================= +Embed in GTK4 with a navigation toolbar +======================================= Demonstrate NavigationToolbar with GTK4 accessed via pygobject. """ @@ -23,7 +23,7 @@ def on_activate(app): win = Gtk.ApplicationWindow(application=app) win.set_default_size(400, 300) - win.set_title("Embedding in GTK4") + win.set_title("Embedded in GTK4") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py index 0e17473de5d3..197cd7971088 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -1,7 +1,7 @@ """ -================= -Embedding in GTK4 -================= +============= +Embed in GTK4 +============= Demonstrate adding a FigureCanvasGTK4Agg widget to a Gtk.ScrolledWindow using GTK4 accessed via pygobject. @@ -22,7 +22,7 @@ def on_activate(app): win = Gtk.ApplicationWindow(application=app) win.set_default_size(400, 300) - win.set_title("Embedding in GTK4") + win.set_title("Embedded in GTK4") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py index b79f582a65e4..cea1a89c29df 100644 --- a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -Embedding in Qt -=============== +=========== +Embed in Qt +=========== Simple Qt application embedding Matplotlib canvases. This program will work equally well using any Qt binding (PyQt6, PySide6, PyQt5, PySide2). The @@ -44,18 +44,34 @@ def __init__(self): self._static_ax.plot(t, np.tan(t), ".") self._dynamic_ax = dynamic_canvas.figure.subplots() - t = np.linspace(0, 10, 101) # Set up a Line2D. - self._line, = self._dynamic_ax.plot(t, np.sin(t + time.time())) - self._timer = dynamic_canvas.new_timer(50) - self._timer.add_callback(self._update_canvas) - self._timer.start() + self.xdata = np.linspace(0, 10, 101) + self._update_ydata() + self._line, = self._dynamic_ax.plot(self.xdata, self.ydata) + # The below two timers must be attributes of self, so that the garbage + # collector won't clean them after we finish with __init__... - def _update_canvas(self): - t = np.linspace(0, 10, 101) + # The data retrieval may be fast as possible (Using QRunnable could be + # even faster). + self.data_timer = dynamic_canvas.new_timer(1) + self.data_timer.add_callback(self._update_ydata) + self.data_timer.start() + # Drawing at 50Hz should be fast enough for the GUI to feel smooth, and + # not too fast for the GUI to be overloaded with events that need to be + # processed while the GUI element is changed. + self.drawing_timer = dynamic_canvas.new_timer(20) + self.drawing_timer.add_callback(self._update_canvas) + self.drawing_timer.start() + + def _update_ydata(self): # Shift the sinusoid as a function of time. - self._line.set_data(t, np.sin(t + time.time())) - self._line.figure.canvas.draw() + self.ydata = np.sin(self.xdata + time.time()) + + def _update_canvas(self): + self._line.set_data(self.xdata, self.ydata) + # It should be safe to use the synchronous draw() method for most drawing + # frequencies, but it is safer to use draw_idle(). + self._line.figure.canvas.draw_idle() if __name__ == "__main__": diff --git a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py index e5c4aa4125b6..7474f40b4bac 100644 --- a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -Embedding in Tk -=============== +=========== +Embed in Tk +=========== """ @@ -16,7 +16,7 @@ from matplotlib.figure import Figure root = tkinter.Tk() -root.wm_title("Embedding in Tk") +root.wm_title("Embedded in Tk") fig = Figure(figsize=(5, 4), dpi=100) t = np.arange(0, 3, .01) diff --git a/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py index acfe1bc9d98f..634d8c511aa7 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #2 -================== +============== +Embed in wx #2 +============== An example of how to use wxagg in an application with the new toolbar - comment out the add_toolbar line for no toolbar. diff --git a/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py index 40282699d872..ac1213be0576 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #3 -================== +============== +Embed in wx #3 +============== Copyright (C) 2003-2004 Andrew Straw, Jeremy O'Donoghue and others diff --git a/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py index b9504ff25dee..062f1219adb5 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #4 -================== +============== +Embed in wx #4 +============== An example of how to use wxagg in a wx application with a custom toolbar. """ diff --git a/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py index 80062782d9fa..f150e2106ead 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #5 -================== +============== +Embed in wx #5 +============== """ diff --git a/galleries/examples/user_interfaces/web_application_server_sgskip.py b/galleries/examples/user_interfaces/web_application_server_sgskip.py index 950109015191..60c321e02eb9 100644 --- a/galleries/examples/user_interfaces/web_application_server_sgskip.py +++ b/galleries/examples/user_interfaces/web_application_server_sgskip.py @@ -1,7 +1,7 @@ """ -============================================= -Embedding in a web application server (Flask) -============================================= +========================================= +Embed in a web application server (Flask) +========================================= When using Matplotlib in a web server it is strongly recommended to not use pyplot (pyplot maintains references to the opened figures to make diff --git a/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py b/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py index 96c6d760dc5d..e2e7348f1c3c 100644 --- a/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py +++ b/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py @@ -1,7 +1,7 @@ """ -===================== -Adding a cursor in WX -===================== +================== +Add a cursor in WX +================== Example to draw a cursor and report the data coords in wx. """ diff --git a/galleries/examples/userdemo/annotate_explain.py b/galleries/examples/userdemo/annotate_explain.py deleted file mode 100644 index 8f20b5406bd7..000000000000 --- a/galleries/examples/userdemo/annotate_explain.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -================ -Annotate Explain -================ - -""" - -import matplotlib.pyplot as plt - -import matplotlib.patches as mpatches - -fig, axs = plt.subplots(2, 2) -x1, y1 = 0.3, 0.3 -x2, y2 = 0.7, 0.7 - -ax = axs.flat[0] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=None, - shrinkB=0, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "connect", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[1] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=el, - shrinkB=0, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "clip", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[2] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=el, - shrinkB=5, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "shrink", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[3] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="fancy", - color="0.5", - patchB=el, - shrinkB=5, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "mutate", transform=ax.transAxes, ha="left", va="top") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) - -plt.show() diff --git a/galleries/examples/userdemo/annotate_text_arrow.py b/galleries/examples/userdemo/annotate_text_arrow.py deleted file mode 100644 index 2495c7687bd7..000000000000 --- a/galleries/examples/userdemo/annotate_text_arrow.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -=================== -Annotate Text Arrow -=================== - -""" - -import matplotlib.pyplot as plt -import numpy as np - -# Fixing random state for reproducibility -np.random.seed(19680801) - -fig, ax = plt.subplots(figsize=(5, 5)) -ax.set_aspect(1) - -x1 = -1 + np.random.randn(100) -y1 = -1 + np.random.randn(100) -x2 = 1. + np.random.randn(100) -y2 = 1. + np.random.randn(100) - -ax.scatter(x1, y1, color="r") -ax.scatter(x2, y2, color="g") - -bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.9) -ax.text(-2, -2, "Sample A", ha="center", va="center", size=20, - bbox=bbox_props) -ax.text(2, 2, "Sample B", ha="center", va="center", size=20, - bbox=bbox_props) - - -bbox_props = dict(boxstyle="rarrow", fc=(0.8, 0.9, 0.9), ec="b", lw=2) -t = ax.text(0, 0, "Direction", ha="center", va="center", rotation=45, - size=15, - bbox=bbox_props) - -bb = t.get_bbox_patch() -bb.set_boxstyle("rarrow", pad=0.6) - -ax.set_xlim(-4, 4) -ax.set_ylim(-4, 4) - -plt.show() diff --git a/galleries/examples/userdemo/connectionstyle_demo.py b/galleries/examples/userdemo/connectionstyle_demo.py deleted file mode 100644 index e34c63a5708b..000000000000 --- a/galleries/examples/userdemo/connectionstyle_demo.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -================================= -Connection styles for annotations -================================= - -When creating an annotation using `~.Axes.annotate`, the arrow shape can be -controlled via the *connectionstyle* parameter of *arrowprops*. For further -details see the description of `.FancyArrowPatch`. -""" - -import matplotlib.pyplot as plt - - -def demo_con_style(ax, connectionstyle): - x1, y1 = 0.3, 0.2 - x2, y2 = 0.8, 0.6 - - ax.plot([x1, x2], [y1, y2], ".") - ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", color="0.5", - shrinkA=5, shrinkB=5, - patchA=None, patchB=None, - connectionstyle=connectionstyle, - ), - ) - - ax.text(.05, .95, connectionstyle.replace(",", ",\n"), - transform=ax.transAxes, ha="left", va="top") - - -fig, axs = plt.subplots(3, 5, figsize=(7, 6.3), layout="constrained") -demo_con_style(axs[0, 0], "angle3,angleA=90,angleB=0") -demo_con_style(axs[1, 0], "angle3,angleA=0,angleB=90") -demo_con_style(axs[0, 1], "arc3,rad=0.") -demo_con_style(axs[1, 1], "arc3,rad=0.3") -demo_con_style(axs[2, 1], "arc3,rad=-0.3") -demo_con_style(axs[0, 2], "angle,angleA=-90,angleB=180,rad=0") -demo_con_style(axs[1, 2], "angle,angleA=-90,angleB=180,rad=5") -demo_con_style(axs[2, 2], "angle,angleA=-90,angleB=10,rad=5") -demo_con_style(axs[0, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=0") -demo_con_style(axs[1, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=5") -demo_con_style(axs[2, 3], "arc,angleA=-90,angleB=0,armA=0,armB=40,rad=0") -demo_con_style(axs[0, 4], "bar,fraction=0.3") -demo_con_style(axs[1, 4], "bar,fraction=-0.3") -demo_con_style(axs[2, 4], "bar,angle=180,fraction=-0.2") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1.25), xticks=[], yticks=[], aspect=1.25) -fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) - -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.annotate` -# - `matplotlib.patches.FancyArrowPatch` diff --git a/galleries/examples/userdemo/custom_boxstyle01.py b/galleries/examples/userdemo/custom_boxstyle01.py deleted file mode 100644 index 71668cc6cc31..000000000000 --- a/galleries/examples/userdemo/custom_boxstyle01.py +++ /dev/null @@ -1,128 +0,0 @@ -r""" -================= -Custom box styles -================= - -This example demonstrates the implementation of a custom `.BoxStyle`. -Custom `.ConnectionStyle`\s and `.ArrowStyle`\s can be similarly defined. -""" - -import matplotlib.pyplot as plt - -from matplotlib.patches import BoxStyle -from matplotlib.path import Path - -# %% -# Custom box styles can be implemented as a function that takes arguments -# specifying both a rectangular box and the amount of "mutation", and -# returns the "mutated" path. The specific signature is the one of -# ``custom_box_style`` below. -# -# Here, we return a new path which adds an "arrow" shape on the left of the -# box. -# -# The custom box style can then be used by passing -# ``bbox=dict(boxstyle=custom_box_style, ...)`` to `.Axes.text`. - - -def custom_box_style(x0, y0, width, height, mutation_size): - """ - Given the location and size of the box, return the path of the box around - it. - - Rotation is automatically taken care of. - - Parameters - ---------- - x0, y0, width, height : float - Box location and size. - mutation_size : float - Mutation reference scale, typically the text font size. - """ - # padding - mypad = 0.3 - pad = mutation_size * mypad - # width and height with padding added. - width = width + 2 * pad - height = height + 2 * pad - # boundary of the padded box - x0, y0 = x0 - pad, y0 - pad - x1, y1 = x0 + width, y0 + height - # return the new path - return Path([(x0, y0), - (x1, y0), (x1, y1), (x0, y1), - (x0-pad, (y0+y1)/2), (x0, y0), - (x0, y0)], - closed=True) - - -fig, ax = plt.subplots(figsize=(3, 3)) -ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, - bbox=dict(boxstyle=custom_box_style, alpha=0.2)) - - -# %% -# Likewise, custom box styles can be implemented as classes that implement -# ``__call__``. -# -# The classes can then be registered into the ``BoxStyle._style_list`` dict, -# which allows specifying the box style as a string, -# ``bbox=dict(boxstyle="registered_name,param=value,...", ...)``. -# Note that this registration relies on internal APIs and is therefore not -# officially supported. - - -class MyStyle: - """A simple box.""" - - def __init__(self, pad=0.3): - """ - The arguments must be floats and have default values. - - Parameters - ---------- - pad : float - amount of padding - """ - self.pad = pad - super().__init__() - - def __call__(self, x0, y0, width, height, mutation_size): - """ - Given the location and size of the box, return the path of the box - around it. - - Rotation is automatically taken care of. - - Parameters - ---------- - x0, y0, width, height : float - Box location and size. - mutation_size : float - Reference scale for the mutation, typically the text font size. - """ - # padding - pad = mutation_size * self.pad - # width and height with padding added - width = width + 2.*pad - height = height + 2.*pad - # boundary of the padded box - x0, y0 = x0 - pad, y0 - pad - x1, y1 = x0 + width, y0 + height - # return the new path - return Path([(x0, y0), - (x1, y0), (x1, y1), (x0, y1), - (x0-pad, (y0+y1)/2.), (x0, y0), - (x0, y0)], - closed=True) - - -BoxStyle._style_list["angled"] = MyStyle # Register the custom style. - -fig, ax = plt.subplots(figsize=(3, 3)) -ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, - bbox=dict(boxstyle="angled,pad=0.5", alpha=0.2)) - -del BoxStyle._style_list["angled"] # Unregister it. - -plt.show() diff --git a/galleries/examples/userdemo/pgf_fonts.py b/galleries/examples/userdemo/pgf_fonts.py deleted file mode 100644 index 9d5f5594b81b..000000000000 --- a/galleries/examples/userdemo/pgf_fonts.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -========= -PGF fonts -========= -""" - -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "font.family": "serif", - # Use LaTeX default serif font. - "font.serif": [], - # Use specific cursive fonts. - "font.cursive": ["Comic Neue", "Comic Sans MS"], -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.text(0.5, 3., "serif") -ax.text(0.5, 2., "monospace", family="monospace") -ax.text(2.5, 2., "sans-serif", family="DejaVu Sans") # Use specific sans font. -ax.text(2.5, 1., "comic", family="cursive") -ax.set_xlabel("µ is not $\\mu$") - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_fonts.pdf") -fig.savefig("pgf_fonts.png") diff --git a/galleries/examples/userdemo/pgf_preamble_sgskip.py b/galleries/examples/userdemo/pgf_preamble_sgskip.py deleted file mode 100644 index b32fa972c31f..000000000000 --- a/galleries/examples/userdemo/pgf_preamble_sgskip.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -============ -PGF preamble -============ -""" - -import matplotlib as mpl - -mpl.use("pgf") -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "font.family": "serif", # use serif/main font for text elements - "text.usetex": True, # use inline math for ticks - "pgf.rcfonts": False, # don't setup fonts from rc parameters - "pgf.preamble": "\n".join([ - r"\usepackage{url}", # load additional packages - r"\usepackage{unicode-math}", # unicode math setup - r"\setmainfont{DejaVu Serif}", # serif font via preamble - ]) -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.set_xlabel("unicode text: я, ψ, €, ü") -ax.set_ylabel(r"\url{https://matplotlib.org}") -ax.legend(["unicode math: $λ=∑_i^∞ μ_i^2$"]) - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_preamble.pdf") -fig.savefig("pgf_preamble.png") diff --git a/galleries/examples/userdemo/pgf_texsystem.py b/galleries/examples/userdemo/pgf_texsystem.py deleted file mode 100644 index 0d8e326803ea..000000000000 --- a/galleries/examples/userdemo/pgf_texsystem.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -============= -PGF texsystem -============= -""" - -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "pgf.texsystem": "pdflatex", - "pgf.preamble": "\n".join([ - r"\usepackage[utf8x]{inputenc}", - r"\usepackage[T1]{fontenc}", - r"\usepackage{cmbright}", - ]), -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.text(0.5, 3., "serif", family="serif") -ax.text(0.5, 2., "monospace", family="monospace") -ax.text(2.5, 2., "sans-serif", family="sans-serif") -ax.set_xlabel(r"µ is not $\mu$") - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_texsystem.pdf") -fig.savefig("pgf_texsystem.png") diff --git a/galleries/examples/userdemo/simple_annotate01.py b/galleries/examples/userdemo/simple_annotate01.py deleted file mode 100644 index cb3b6cb7e2c8..000000000000 --- a/galleries/examples/userdemo/simple_annotate01.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -================= -Simple Annotate01 -================= - -""" - -import matplotlib.pyplot as plt - -import matplotlib.patches as mpatches - -fig, axs = plt.subplots(2, 4) -x1, y1 = 0.3, 0.3 -x2, y2 = 0.7, 0.7 - -ax = axs.flat[0] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->")) -ax.text(.05, .95, "A $->$ B", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[2] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.3", - shrinkB=5)) -ax.text(.05, .95, "shrinkB=5", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[3] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.3")) -ax.text(.05, .95, "connectionstyle=arc3", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[4] -ax.plot([x1, x2], [y1, y2], "o") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.5) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.2")) - -ax = axs.flat[5] -ax.plot([x1, x2], [y1, y2], "o") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.5) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.2", - patchB=el)) -ax.text(.05, .95, "patchB", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[6] -ax.plot([x1], [y1], "o") -ax.annotate("Test", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - ha="center", va="center", - bbox=dict(boxstyle="round", fc="w"), - arrowprops=dict(arrowstyle="->")) -ax.text(.05, .95, "annotate", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[7] -ax.plot([x1], [y1], "o") -ax.annotate("Test", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - ha="center", va="center", - bbox=dict(boxstyle="round", fc="w", ), - arrowprops=dict(arrowstyle="->", relpos=(0., 0.))) -ax.text(.05, .95, "relpos=(0, 0)", - transform=ax.transAxes, ha="left", va="top") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) - -plt.show() diff --git a/galleries/examples/widgets/polygon_selector_simple.py b/galleries/examples/widgets/polygon_selector_simple.py index 8dab957cdca0..e344da7e0645 100644 --- a/galleries/examples/widgets/polygon_selector_simple.py +++ b/galleries/examples/widgets/polygon_selector_simple.py @@ -38,6 +38,15 @@ # %% +# .. tags:: +# +# component: axes, +# styling: position, +# plot-type: line, +# level: intermediate, +# domain: cartography, +# domain: geometry, +# domain: statistics, # # .. admonition:: References # diff --git a/galleries/examples/widgets/range_slider.py b/galleries/examples/widgets/range_slider.py index 1ae40c9841fe..f1bed7431e39 100644 --- a/galleries/examples/widgets/range_slider.py +++ b/galleries/examples/widgets/range_slider.py @@ -1,7 +1,7 @@ """ -====================================== -Thresholding an Image with RangeSlider -====================================== +================================= +Image scaling using a RangeSlider +================================= Using the RangeSlider widget to control the thresholding of an image. diff --git a/galleries/examples/widgets/slider_snap_demo.py b/galleries/examples/widgets/slider_snap_demo.py index 23910415da8f..953ffaf63672 100644 --- a/galleries/examples/widgets/slider_snap_demo.py +++ b/galleries/examples/widgets/slider_snap_demo.py @@ -1,7 +1,7 @@ """ -=================================== -Snapping Sliders to Discrete Values -=================================== +=============================== +Snap sliders to discrete values +=============================== You can snap slider values to discrete values using the ``valstep`` argument. diff --git a/galleries/plot_types/3D/fill_between3d_simple.py b/galleries/plot_types/3D/fill_between3d_simple.py new file mode 100644 index 000000000000..f12fbbb5e958 --- /dev/null +++ b/galleries/plot_types/3D/fill_between3d_simple.py @@ -0,0 +1,33 @@ +""" +==================================== +fill_between(x1, y1, z1, x2, y2, z2) +==================================== + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.fill_between`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# Make data for a double helix +n = 50 +theta = np.linspace(0, 2*np.pi, n) +x1 = np.cos(theta) +y1 = np.sin(theta) +z1 = np.linspace(0, 1, n) +x2 = np.cos(theta + np.pi) +y2 = np.sin(theta + np.pi) +z2 = z1 + +# Plot +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) +ax.fill_between(x1, y1, z1, x2, y2, z2, alpha=0.5) +ax.plot(x1, y1, z1, linewidth=2, color='C0') +ax.plot(x2, y2, z2, linewidth=2, color='C0') + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index f5e4589e8a52..a258eb71d447 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -11,15 +11,15 @@ There are three layers to the Matplotlib API. -* the :class:`matplotlib.backend_bases.FigureCanvas` is the area onto which +* the :class:`!matplotlib.backend_bases.FigureCanvas` is the area onto which the figure is drawn -* the :class:`matplotlib.backend_bases.Renderer` is the object which knows how - to draw on the :class:`~matplotlib.backend_bases.FigureCanvas` +* the :class:`!matplotlib.backend_bases.Renderer` is the object which knows how + to draw on the :class:`!matplotlib.backend_bases.FigureCanvas` * and the :class:`matplotlib.artist.Artist` is the object that knows how to use a renderer to paint onto the canvas. -The :class:`~matplotlib.backend_bases.FigureCanvas` and -:class:`~matplotlib.backend_bases.Renderer` handle all the details of +The :class:`!matplotlib.backend_bases.FigureCanvas` and +:class:`!matplotlib.backend_bases.Renderer` handle all the details of talking to user interface toolkits like `wxPython `_ or drawing languages like PostScript®, and the ``Artist`` handles all the high level constructs like representing @@ -156,12 +156,10 @@ class in the Matplotlib API, and the one you will be working with most # (the standard white box with black edges in the typical Matplotlib # plot, has a ``Rectangle`` instance that determines the color, # transparency, and other properties of the Axes. These instances are -# stored as member variables :attr:`Figure.patch -# ` and :attr:`Axes.patch -# ` ("Patch" is a name inherited from -# MATLAB, and is a 2D "patch" of color on the figure, e.g., rectangles, -# circles and polygons). Every Matplotlib ``Artist`` has the following -# properties +# stored as member variables :attr:`!Figure.patch` and :attr:`!Axes.patch` +# ("Patch" is a name inherited from MATLAB, and is a 2D "patch" +# of color on the figure, e.g., rectangles, circles and polygons). +# Every Matplotlib ``Artist`` has the following properties # # ========== ================================================================= # Property Description @@ -284,7 +282,7 @@ class in the Matplotlib API, and the one you will be working with most # :class:`matplotlib.figure.Figure`, and it contains everything in the # figure. The background of the figure is a # :class:`~matplotlib.patches.Rectangle` which is stored in -# :attr:`Figure.patch `. As +# :attr:`!Figure.patch`. As # you add subplots (:meth:`~matplotlib.figure.Figure.add_subplot`) and # Axes (:meth:`~matplotlib.figure.Figure.add_axes`) to the figure # these will be appended to the :attr:`Figure.axes @@ -331,8 +329,7 @@ class in the Matplotlib API, and the one you will be working with most # # As with all ``Artist``\s, you can control this coordinate system by setting # the transform property. You can explicitly use "figure coordinates" by -# setting the ``Artist`` transform to :attr:`fig.transFigure -# `: +# setting the ``Artist`` transform to :attr:`!fig.transFigure`: import matplotlib.lines as lines @@ -375,7 +372,7 @@ class in the Matplotlib API, and the one you will be working with most # customize the ``Artists`` it contains. Like the # :class:`~matplotlib.figure.Figure`, it contains a # :class:`~matplotlib.patches.Patch` -# :attr:`~matplotlib.axes.Axes.patch` which is a +# :attr:`!matplotlib.axes.Axes.patch` which is a # :class:`~matplotlib.patches.Rectangle` for Cartesian coordinates and a # :class:`~matplotlib.patches.Circle` for polar coordinates; this patch # determines the shape, background and border of the plotting region:: @@ -408,8 +405,7 @@ class in the Matplotlib API, and the one you will be working with most # # Similarly, methods that create patches, like # :meth:`~matplotlib.axes.Axes.bar` creates a list of rectangles, will -# add the patches to the :attr:`Axes.patches -# ` list: +# add the patches to the :attr:`!Axes.patches` list: # # .. sourcecode:: ipython # @@ -556,8 +552,8 @@ class in the Matplotlib API, and the one you will be working with most # important ``Artist`` containers: the :class:`~matplotlib.axis.XAxis` # and :class:`~matplotlib.axis.YAxis`, which handle the drawing of the # ticks and labels. These are stored as instance variables -# :attr:`~matplotlib.axes.Axes.xaxis` and -# :attr:`~matplotlib.axes.Axes.yaxis`. The ``XAxis`` and ``YAxis`` +# :attr:`!matplotlib.axes.Axes.xaxis` and +# :attr:`!matplotlib.axes.Axes.yaxis`. The ``XAxis`` and ``YAxis`` # containers will be detailed below, but note that the ``Axes`` contains # many helper methods which forward calls on to the # :class:`~matplotlib.axis.Axis` instances, so you often do not need to diff --git a/galleries/users_explain/animations/animations.py b/galleries/users_explain/animations/animations.py index 2711663196f2..a0669956ab81 100644 --- a/galleries/users_explain/animations/animations.py +++ b/galleries/users_explain/animations/animations.py @@ -251,3 +251,8 @@ def update(frame): # writer="imagemagick", extra_args=["-quality", "100"]) # # (the ``extra_args`` for *apng* are needed to reduce filesize by ~10x) +# +# Note that *ffmpeg* and *imagemagick* need to be separately installed. +# A cross-platform way to obtain *ffmpeg* is to install the ``imageio_ffmpeg`` +# PyPI package, and then to set +# ``rcParams["animation.ffmpeg_path"] = imageio_ffmpeg.get_ffmpeg_exe()``. diff --git a/galleries/users_explain/artists/transforms_tutorial.py b/galleries/users_explain/artists/transforms_tutorial.py index 8eed53c812b8..0be5fa3c2e21 100644 --- a/galleries/users_explain/artists/transforms_tutorial.py +++ b/galleries/users_explain/artists/transforms_tutorial.py @@ -22,6 +22,7 @@ :class:`~matplotlib.figure.Figure` instance, and ``subfigure`` is a :class:`~matplotlib.figure.SubFigure` instance. +.. _coordinate-systems: +----------------+-----------------------------------+-----------------------------+ |Coordinate |Description |Transformation object | diff --git a/galleries/users_explain/axes/arranging_axes.py b/galleries/users_explain/axes/arranging_axes.py index 1f910393a9da..bc537e15c12c 100644 --- a/galleries/users_explain/axes/arranging_axes.py +++ b/galleries/users_explain/axes/arranging_axes.py @@ -308,7 +308,7 @@ def annotate_axes(ax, text, fontsize=18): # ------------------------------------- # # We can index the *spec* array using `NumPy slice syntax -# `_ +# `_ # and the new Axes will span the slice. This would be the same # as ``fig, axd = plt.subplot_mosaic([['ax0', 'ax0'], ['ax1', 'ax2']], ...)``: diff --git a/galleries/users_explain/axes/axes_units.py b/galleries/users_explain/axes/axes_units.py index 6b486f007a12..a9159abe33ce 100644 --- a/galleries/users_explain/axes/axes_units.py +++ b/galleries/users_explain/axes/axes_units.py @@ -236,7 +236,7 @@ x = np.arange(100) ax = axs[0] ax.plot(x, x) -label = f'Converter: {ax.xaxis.converter}\n ' +label = f'Converter: {ax.xaxis.get_converter()}\n ' label += f'Locator: {ax.xaxis.get_major_locator()}\n' label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' ax.set_xlabel(label) @@ -245,7 +245,7 @@ time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]') x = np.arange(len(time)) ax.plot(time, x) -label = f'Converter: {ax.xaxis.converter}\n ' +label = f'Converter: {ax.xaxis.get_converter()}\n ' label += f'Locator: {ax.xaxis.get_major_locator()}\n' label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' ax.set_xlabel(label) @@ -255,7 +255,7 @@ names = list(data.keys()) values = list(data.values()) ax.plot(names, values) -label = f'Converter: {ax.xaxis.converter}\n ' +label = f'Converter: {ax.xaxis.get_converter()}\n ' label += f'Locator: {ax.xaxis.get_major_locator()}\n' label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' ax.set_xlabel(label) diff --git a/galleries/users_explain/colors/colormaps.py b/galleries/users_explain/colors/colormaps.py index 92b56d298976..ff146cacf170 100644 --- a/galleries/users_explain/colors/colormaps.py +++ b/galleries/users_explain/colors/colormaps.py @@ -175,10 +175,15 @@ def plot_color_gradients(category, cmap_list): # equal minimum :math:`L^*` values at opposite ends of the colormap. By these # measures, BrBG and RdBu are good options. coolwarm is a good option, but it # doesn't span a wide range of :math:`L^*` values (see grayscale section below). +# +# Berlin, Managua, and Vanimo are dark-mode diverging colormaps, with minimum +# lightness at the center, and maximum at the extremes. These are taken from +# F. Crameri's [scientific colour maps]_ version 8.0.1. plot_color_gradients('Diverging', ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', - 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']) + 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', + 'berlin', 'managua', 'vanimo']) # %% # Cyclic @@ -441,3 +446,4 @@ def plot_color_gradients(cmap_category, cmap_list): # .. [colorblindness] http://www.color-blindness.com/ # .. [IBM] https://doi.org/10.1109/VISUAL.1995.480803 # .. [turbo] https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html +# .. [scientific colour maps] https://doi.org/10.5281/zenodo.1243862 diff --git a/galleries/users_explain/colors/colors.py b/galleries/users_explain/colors/colors.py index 9b6a3832b75c..c91a5fcb0dbe 100644 --- a/galleries/users_explain/colors/colors.py +++ b/galleries/users_explain/colors/colors.py @@ -73,6 +73,10 @@ | | | | .. versionadded:: 3.8 | | +--------------------------------------+--------------------------------------+ +| The special value "none" is fully | - ``'none'`` | +| transparent, i.e. equivalent to a | | +| RGBA value ``(0.0, 0.0, 0.0, 0.0)`` | | ++--------------------------------------+--------------------------------------+ .. _xkcd color survey: https://xkcd.com/color/rgb/ diff --git a/galleries/users_explain/customizing.py b/galleries/users_explain/customizing.py index b0aaee03239e..05b75ba7d0a4 100644 --- a/galleries/users_explain/customizing.py +++ b/galleries/users_explain/customizing.py @@ -234,8 +234,8 @@ def plotting_function(): # # 4. :file:`{INSTALL}/matplotlib/mpl-data/matplotlibrc`, where # :file:`{INSTALL}` is something like -# :file:`/usr/lib/python3.9/site-packages` on Linux, and maybe -# :file:`C:\\Python39\\Lib\\site-packages` on Windows. Every time you +# :file:`/usr/lib/python3.10/site-packages` on Linux, and maybe +# :file:`C:\\Python310\\Lib\\site-packages` on Windows. Every time you # install matplotlib, this file will be overwritten, so if you want # your customizations to be saved, please move this file to your # user-specific matplotlib directory. diff --git a/galleries/users_explain/figure/api_interfaces.rst b/galleries/users_explain/figure/api_interfaces.rst index 177e516cdaa0..981359dbee0b 100644 --- a/galleries/users_explain/figure/api_interfaces.rst +++ b/galleries/users_explain/figure/api_interfaces.rst @@ -121,6 +121,51 @@ In the explicit interface, this would be: axs[0].plot([1, 2, 3], [0, 0.5, 0.2]) axs[1].plot([3, 2, 1], [0, 0.5, 0.2]) +Translating between the Axes interface and the pyplot interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You may find either interface in existing code, and unfortunately sometimes even +mixtures. This section describes the patterns for specific operations in both +interfaces and how to translate from one to the other. + +- Creating figures is the same for both interfaces: Use the respective `.pyplot` + functions ``plt.figure()``, ``plt.subplots()``, ``plt.subplot_mosaic()``. + For the Axes interface, you typically store the created Figure (and possibly + Axes) in variables for later use. When using the pyplot interface, these + values are typically not stored. Example: + + - Axes: ``fig, ax = plt.subplots()`` + - pyplot: ``plt.subplots()`` + +- "Plotting" functions, i.e. functions that add data, are named the same and + have identical parameters on the Axes and in pyplot. Example: + + - Axes: ``ax.plot(x, y)`` + - pyplot: ``plt.plot(x, y)`` + +- Functions that retrieve properties are named like the property in pyplot + and are prefixed with ``get_`` on the Axes. Example: + + - Axes: ``label = ax.get_xlabel()`` + - pyplot: ``label = plt.xlabel()`` + +- Functions that set properties like the property in pyplot and are prefixed with + ``set_`` on the Axes. Example: + + - Axes: ``ax.set_xlabel("time")`` + - pyplot: ``plt.xlabel("time")`` + +Here is a short summary of the examples again as a side-by-side comparison: + +================== ============================ ======================== +Operation Axes interface pyplot interface +================== ============================ ======================== +Creating figures ``fig, ax = plt.subplots()`` ``plt.subplots()`` +Plotting data ``ax.plot(x, y)`` ``plt.plot(x, y)`` +Getting properties ``label = ax.get_xlabel()`` ``label = plt.xlabel()`` +Setting properties ``ax.set_xlabel("time")`` ``plt.xlabel("time")`` +================== ============================ ======================== + + Why be explicit? ^^^^^^^^^^^^^^^^ diff --git a/galleries/users_explain/figure/backends.rst b/galleries/users_explain/figure/backends.rst index dc6d8a89457d..b808d5951fd8 100644 --- a/galleries/users_explain/figure/backends.rst +++ b/galleries/users_explain/figure/backends.rst @@ -211,7 +211,7 @@ wxAgg Agg rendering to a wxWidgets_ canvas (requires wxPython_ 4). .. _`Scalable Vector Graphics`: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics .. _pgf: https://ctan.org/pkg/pgf .. _Cairo: https://www.cairographics.org -.. _PyGObject: https://wiki.gnome.org/action/show/Projects/PyGObject +.. _PyGObject: https://pygobject.gnome.org/ .. _pycairo: https://www.cairographics.org/pycairo/ .. _cairocffi: https://doc.courtbouillon.org/cairocffi/stable/ .. _wxPython: https://www.wxpython.org/ diff --git a/galleries/users_explain/figure/writing_a_backend_pyplot_interface.rst b/galleries/users_explain/figure/writing_a_backend_pyplot_interface.rst index c8dccc24da43..5325b3d9ba4c 100644 --- a/galleries/users_explain/figure/writing_a_backend_pyplot_interface.rst +++ b/galleries/users_explain/figure/writing_a_backend_pyplot_interface.rst @@ -107,7 +107,7 @@ module:// syntax Any backend in a separate module (not built into Matplotlib) can be used by specifying the path to the module in the form ``module://some.backend.module``. An example is ``module://mplcairo.qt`` for -`mplcairo `_. The backend's +`mplcairo `_. The backend's interactive framework will be taken from its ``FigureCanvas.required_interactive_framework``. diff --git a/galleries/users_explain/quick_start.py b/galleries/users_explain/quick_start.py index 30740f74b898..f24d90e8495c 100644 --- a/galleries/users_explain/quick_start.py +++ b/galleries/users_explain/quick_start.py @@ -497,7 +497,7 @@ def my_plotter(ax, data1, data2, param_dict): ax3.plot(t, s) ax3.set_xlabel('Angle [rad]') -ax4 = ax3.secondary_xaxis('top', functions=(np.rad2deg, np.deg2rad)) +ax4 = ax3.secondary_xaxis('top', (np.rad2deg, np.deg2rad)) ax4.set_xlabel('Angle [°]') # %% diff --git a/galleries/users_explain/text/annotations.py b/galleries/users_explain/text/annotations.py index 89787c4a6336..5cfb16c12715 100644 --- a/galleries/users_explain/text/annotations.py +++ b/galleries/users_explain/text/annotations.py @@ -1,13 +1,16 @@ r""" +.. redirect-from:: /gallery/userdemo/anchored_box04 +.. redirect-from:: /gallery/userdemo/annotate_explain .. redirect-from:: /gallery/userdemo/annotate_simple01 .. redirect-from:: /gallery/userdemo/annotate_simple02 .. redirect-from:: /gallery/userdemo/annotate_simple03 .. redirect-from:: /gallery/userdemo/annotate_simple04 -.. redirect-from:: /gallery/userdemo/anchored_box04 .. redirect-from:: /gallery/userdemo/annotate_simple_coord01 .. redirect-from:: /gallery/userdemo/annotate_simple_coord02 .. redirect-from:: /gallery/userdemo/annotate_simple_coord03 +.. redirect-from:: /gallery/userdemo/annotate_text_arrow .. redirect-from:: /gallery/userdemo/connect_simple01 +.. redirect-from:: /gallery/userdemo/connectionstyle_demo .. redirect-from:: /tutorials/text/annotations .. _annotations: @@ -265,23 +268,30 @@ # Defining custom box styles # ^^^^^^^^^^^^^^^^^^^^^^^^^^ # -# You can use a custom box style. The value for the ``boxstyle`` can be a -# callable object in the following forms: +# Custom box styles can be implemented as a function that takes arguments specifying +# both a rectangular box and the amount of "mutation", and returns the "mutated" path. +# The specific signature is the one of ``custom_box_style`` below. +# +# Here, we return a new path which adds an "arrow" shape on the left of the box. +# +# The custom box style can then be used by passing +# ``bbox=dict(boxstyle=custom_box_style, ...)`` to `.Axes.text`. from matplotlib.path import Path def custom_box_style(x0, y0, width, height, mutation_size): """ - Given the location and size of the box, return the path of the box around - it. Rotation is automatically taken care of. + Given the location and size of the box, return the path of the box around it. + + Rotation is automatically taken care of. Parameters ---------- x0, y0, width, height : float Box location and size. mutation_size : float - Mutation reference scale, typically the text font size. + Mutation reference scale, typically the text font size. """ # padding mypad = 0.3 @@ -302,9 +312,71 @@ def custom_box_style(x0, y0, width, height, mutation_size): bbox=dict(boxstyle=custom_box_style, alpha=0.2)) # %% -# See also :doc:`/gallery/userdemo/custom_boxstyle01`. Similarly, you can define a -# custom `.ConnectionStyle` and a custom `.ArrowStyle`. View the source code at -# `.patches` to learn how each class is defined. +# Likewise, custom box styles can be implemented as classes that implement +# ``__call__``. +# +# The classes can then be registered into the ``BoxStyle._style_list`` dict, +# which allows specifying the box style as a string, +# ``bbox=dict(boxstyle="registered_name,param=value,...", ...)``. +# Note that this registration relies on internal APIs and is therefore not +# officially supported. + +from matplotlib.patches import BoxStyle + + +class MyStyle: + """A simple box.""" + + def __init__(self, pad=0.3): + """ + The arguments must be floats and have default values. + + Parameters + ---------- + pad : float + amount of padding + """ + self.pad = pad + super().__init__() + + def __call__(self, x0, y0, width, height, mutation_size): + """ + Given the location and size of the box, return the path of the box around it. + + Rotation is automatically taken care of. + + Parameters + ---------- + x0, y0, width, height : float + Box location and size. + mutation_size : float + Reference scale for the mutation, typically the text font size. + """ + # padding + pad = mutation_size * self.pad + # width and height with padding added + width = width + 2 * pad + height = height + 2 * pad + # boundary of the padded box + x0, y0 = x0 - pad, y0 - pad + x1, y1 = x0 + width, y0 + height + # return the new path + return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), + (x0-pad, (y0+y1)/2), (x0, y0), (x0, y0)], + closed=True) + + +BoxStyle._style_list["angled"] = MyStyle # Register the custom style. + +fig, ax = plt.subplots(figsize=(3, 3)) +ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, + bbox=dict(boxstyle="angled,pad=0.5", alpha=0.2)) + +del BoxStyle._style_list["angled"] # Unregister it. + +# %% +# Similarly, you can define a custom `.ConnectionStyle` and a custom `.ArrowStyle`. View +# the source code at `.patches` to learn how each class is defined. # # .. _annotation_with_custom_arrow: # @@ -332,9 +404,40 @@ def custom_box_style(x0, y0, width, height, mutation_size): # 4. The path is transmuted to an arrow patch, as specified by the *arrowstyle* # parameter. # -# .. figure:: /gallery/userdemo/images/sphx_glr_annotate_explain_001.png -# :target: /gallery/userdemo/annotate_explain.html -# :align: center +# .. plot:: +# :show-source-link: False +# +# import matplotlib.patches as mpatches +# +# x1, y1 = 0.3, 0.3 +# x2, y2 = 0.7, 0.7 +# arrowprops = { +# "1. connect with connectionstyle": +# dict(arrowstyle="-", patchB=False, shrinkB=0), +# "2. clip against patchB": dict(arrowstyle="-", patchB=True, shrinkB=0), +# "3. shrink by shrinkB": dict(arrowstyle="-", patchB=True, shrinkB=5), +# "4. mutate with arrowstyle": dict(arrowstyle="fancy", patchB=True, shrinkB=5), +# } +# +# fig, axs = plt.subplots(2, 2, figsize=(6, 6), layout='compressed') +# for ax, (name, props) in zip(axs.flat, arrowprops.items()): +# ax.plot([x1, x2], [y1, y2], ".") +# +# el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) +# ax.add_artist(el) +# +# props["patchB"] = el if props["patchB"] else None +# +# ax.annotate( +# "", +# xy=(x1, y1), xycoords='data', +# xytext=(x2, y2), textcoords='data', +# arrowprops={"color": "0.5", "connectionstyle": "arc3,rad=0.3", **props}) +# ax.text(.05, .95, name, transform=ax.transAxes, ha="left", va="top") +# +# ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) +# +# fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) # # The creation of the connecting path between two points is controlled by # ``connectionstyle`` key and the following styles are available: @@ -358,9 +461,47 @@ def custom_box_style(x0, y0, width, height, mutation_size): # example below. (Warning: The behavior of the ``bar`` style is currently not # well-defined and may be changed in the future). # -# .. figure:: /gallery/userdemo/images/sphx_glr_connectionstyle_demo_001.png -# :target: /gallery/userdemo/connectionstyle_demo.html -# :align: center +# .. plot:: +# :caption: Connection styles for annotations +# +# def demo_con_style(ax, connectionstyle): +# x1, y1 = 0.3, 0.2 +# x2, y2 = 0.8, 0.6 +# +# ax.plot([x1, x2], [y1, y2], ".") +# ax.annotate("", +# xy=(x1, y1), xycoords='data', +# xytext=(x2, y2), textcoords='data', +# arrowprops=dict(arrowstyle="->", color="0.5", +# shrinkA=5, shrinkB=5, +# patchA=None, patchB=None, +# connectionstyle=connectionstyle, +# ), +# ) +# +# ax.text(.05, .95, connectionstyle.replace(",", ",\n"), +# transform=ax.transAxes, ha="left", va="top") +# +# ax.set(xlim=(0, 1), ylim=(0, 1.25), xticks=[], yticks=[], aspect=1.25) +# +# fig, axs = plt.subplots(3, 5, figsize=(7, 6.3), layout="compressed") +# demo_con_style(axs[0, 0], "angle3,angleA=90,angleB=0") +# demo_con_style(axs[1, 0], "angle3,angleA=0,angleB=90") +# demo_con_style(axs[0, 1], "arc3,rad=0.") +# demo_con_style(axs[1, 1], "arc3,rad=0.3") +# demo_con_style(axs[2, 1], "arc3,rad=-0.3") +# demo_con_style(axs[0, 2], "angle,angleA=-90,angleB=180,rad=0") +# demo_con_style(axs[1, 2], "angle,angleA=-90,angleB=180,rad=5") +# demo_con_style(axs[2, 2], "angle,angleA=-90,angleB=10,rad=5") +# demo_con_style(axs[0, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=0") +# demo_con_style(axs[1, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=5") +# demo_con_style(axs[2, 3], "arc,angleA=-90,angleB=0,armA=0,armB=40,rad=0") +# demo_con_style(axs[0, 4], "bar,fraction=0.3") +# demo_con_style(axs[1, 4], "bar,fraction=-0.3") +# demo_con_style(axs[2, 4], "bar,angle=180,fraction=-0.2") +# +# axs[2, 0].remove() +# fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) # # The connecting path (after clipping and shrinking) is then mutated to # an arrow patch, according to the given ``arrowstyle``: diff --git a/galleries/users_explain/text/pgf.py b/galleries/users_explain/text/pgf.py index 9bcfe34a24b7..fd7693cf55e3 100644 --- a/galleries/users_explain/text/pgf.py +++ b/galleries/users_explain/text/pgf.py @@ -91,6 +91,8 @@ pdf.savefig(fig2) +.. redirect-from:: /gallery/userdemo/pgf_fonts + Font specification ================== @@ -107,9 +109,29 @@ When saving to ``.pgf``, the font configuration Matplotlib used for the layout of the figure is included in the header of the text file. -.. literalinclude:: /gallery/userdemo/pgf_fonts.py - :end-before: fig.savefig +.. code-block:: python + + import matplotlib.pyplot as plt + + plt.rcParams.update({ + "font.family": "serif", + # Use LaTeX default serif font. + "font.serif": [], + # Use specific cursive fonts. + "font.cursive": ["Comic Neue", "Comic Sans MS"], + }) + fig, ax = plt.subplots(figsize=(4.5, 2.5)) + + ax.plot(range(5)) + + ax.text(0.5, 3., "serif") + ax.text(0.5, 2., "monospace", family="monospace") + ax.text(2.5, 2., "sans-serif", family="DejaVu Sans") # Use specific sans font. + ax.text(2.5, 1., "comic", family="cursive") + ax.set_xlabel("µ is not $\\mu$") + +.. redirect-from:: /gallery/userdemo/pgf_preamble_sgskip .. _pgf-preamble: @@ -122,16 +144,33 @@ if you want to do the font configuration yourself instead of using the fonts specified in the rc parameters, make sure to disable :rc:`pgf.rcfonts`. -.. only:: html +.. code-block:: python + + import matplotlib as mpl - .. literalinclude:: /gallery/userdemo/pgf_preamble_sgskip.py - :end-before: fig.savefig + mpl.use("pgf") + import matplotlib.pyplot as plt + + plt.rcParams.update({ + "font.family": "serif", # use serif/main font for text elements + "text.usetex": True, # use inline math for ticks + "pgf.rcfonts": False, # don't setup fonts from rc parameters + "pgf.preamble": "\n".join([ + r"\usepackage{url}", # load additional packages + r"\usepackage{unicode-math}", # unicode math setup + r"\setmainfont{DejaVu Serif}", # serif font via preamble + ]) + }) + + fig, ax = plt.subplots(figsize=(4.5, 2.5)) -.. only:: latex + ax.plot(range(5)) - .. literalinclude:: /gallery/userdemo/pgf_preamble_sgskip.py - :end-before: import matplotlib.pyplot as plt + ax.set_xlabel("unicode text: я, ψ, €, ü") + ax.set_ylabel(r"\url{https://matplotlib.org}") + ax.legend(["unicode math: $λ=∑_i^∞ μ_i^2$"]) +.. redirect-from:: /gallery/userdemo/pgf_texsystem .. _pgf-texsystem: @@ -143,19 +182,33 @@ Please note that when selecting pdflatex, the fonts and Unicode handling must be configured in the preamble. -.. literalinclude:: /gallery/userdemo/pgf_texsystem.py - :end-before: fig.savefig +.. code-block:: python + + import matplotlib.pyplot as plt + + plt.rcParams.update({ + "pgf.texsystem": "pdflatex", + "pgf.preamble": "\n".join([ + r"\usepackage[utf8x]{inputenc}", + r"\usepackage[T1]{fontenc}", + r"\usepackage{cmbright}", + ]), + }) + + fig, ax = plt.subplots(figsize=(4.5, 2.5)) + ax.plot(range(5)) + + ax.text(0.5, 3., "serif", family="serif") + ax.text(0.5, 2., "monospace", family="monospace") + ax.text(2.5, 2., "sans-serif", family="sans-serif") + ax.set_xlabel(r"µ is not $\mu$") .. _pgf-troubleshooting: Troubleshooting =============== -* Please note that the TeX packages found in some Linux distributions and - MiKTeX installations are dramatically outdated. Make sure to update your - package catalog and upgrade or install a recent TeX distribution. - * On Windows, the :envvar:`PATH` environment variable may need to be modified to include the directories containing the latex, dvipng and ghostscript executables. See :ref:`environment-variables` and @@ -175,7 +228,7 @@ * Configuring an ``unicode-math`` environment can be a bit tricky. The TeXLive distribution for example provides a set of math fonts which are - usually not installed system-wide. XeTeX, unlike LuaLatex, cannot find + usually not installed system-wide. XeLaTeX, unlike LuaLaTeX, cannot find these fonts by their name, which is why you might have to specify ``\setmathfont{xits-math.otf}`` instead of ``\setmathfont{XITS Math}`` or alternatively make the fonts available to your OS. See this diff --git a/galleries/users_explain/text/text_intro.py b/galleries/users_explain/text/text_intro.py index 3b8a66f1c98e..29210021369c 100644 --- a/galleries/users_explain/text/text_intro.py +++ b/galleries/users_explain/text/text_intro.py @@ -8,8 +8,6 @@ Text in Matplotlib ================== -Introduction to plotting and working with text in Matplotlib. - Matplotlib has extensive text support, including support for mathematical expressions, truetype support for raster and vector outputs, newline separated text with arbitrary @@ -124,8 +122,8 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('Time [s]') -ax.set_ylabel('Damped oscillation [V]') +ax.set_xlabel('Time (s)') +ax.set_ylabel('Damped oscillation (V)') plt.show() @@ -137,26 +135,26 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1*10000) -ax.set_xlabel('Time [s]') -ax.set_ylabel('Damped oscillation [V]') +ax.set_xlabel('Time (s)') +ax.set_ylabel('Damped oscillation (V)') plt.show() # %% # If you want to move the labels, you can specify the *labelpad* keyword # argument, where the value is points (1/72", the same unit used to specify -# fontsizes). +# font sizes). fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1*10000) -ax.set_xlabel('Time [s]') -ax.set_ylabel('Damped oscillation [V]', labelpad=18) +ax.set_xlabel('Time (s)') +ax.set_ylabel('Damped oscillation (V)', labelpad=18) plt.show() # %% -# Or, the labels accept all the `.Text` keyword arguments, including +# Alternatively, the labels accept all the `.Text` keyword arguments, including # *position*, via which we can manually specify the label positions. Here we # put the xlabel to the far left of the axis. Note, that the y-coordinate of # this position has no effect - to adjust the y-position we need to use the @@ -165,15 +163,15 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('Time [s]', position=(0., 1e6), horizontalalignment='left') -ax.set_ylabel('Damped oscillation [V]') +ax.set_xlabel('Time (s)', position=(0., 1e6), horizontalalignment='left') +ax.set_ylabel('Damped oscillation (V)') plt.show() # %% # All the labelling in this tutorial can be changed by manipulating the # `matplotlib.font_manager.FontProperties` method, or by named keyword -# arguments to `~matplotlib.axes.Axes.set_xlabel` +# arguments to `~matplotlib.axes.Axes.set_xlabel`. from matplotlib.font_manager import FontProperties @@ -182,8 +180,8 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('Time [s]', fontsize='large', fontweight='bold') -ax.set_ylabel('Damped oscillation [V]', fontproperties=font) +ax.set_xlabel('Time (s)', fontsize='large', fontweight='bold') +ax.set_ylabel('Damped oscillation (V)', fontproperties=font) plt.show() @@ -194,8 +192,8 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.2, left=0.2) ax.plot(x1, np.cumsum(y1**2)) -ax.set_xlabel('Time [s] \n This was a long experiment') -ax.set_ylabel(r'$\int\ Y^2\ dt\ \ [V^2 s]$') +ax.set_xlabel('Time (s) \n This was a long experiment') +ax.set_ylabel(r'$\int\ Y^2\ dt\ \ (V^2 s)$') plt.show() @@ -204,14 +202,14 @@ # ====== # # Subplot titles are set in much the same way as labels, but there is -# the *loc* keyword arguments that can change the position and justification -# from the default value of ``loc=center``. +# the *loc* keyword argument that can change the position and justification +# (the default value is "center"). fig, axs = plt.subplots(3, 1, figsize=(5, 6), tight_layout=True) locs = ['center', 'left', 'right'] for ax, loc in zip(axs, locs): ax.plot(x1, y1) - ax.set_title('Title with loc at '+loc, loc=loc) + ax.set_title('Title with loc at ' + loc, loc=loc) plt.show() # %% @@ -237,7 +235,7 @@ # Terminology # ^^^^^^^^^^^ # -# *Axes* have an `matplotlib.axis.Axis` object for the ``ax.xaxis`` and +# *Axes* have a `matplotlib.axis.Axis` object for the ``ax.xaxis`` and # ``ax.yaxis`` that contain the information about how the labels in the axis # are laid out. # @@ -255,9 +253,9 @@ # # It is often convenient to simply define the # tick values, and sometimes the tick labels, overriding the default -# locators and formatters. This is discouraged because it breaks interactive -# navigation of the plot. It also can reset the axis limits: note that -# the second plot has the ticks we asked for, including ones that are +# locators and formatters. However, this is discouraged because it breaks +# interactive navigation of the plot. It also can reset the axis limits: note +# that the second plot has the ticks we asked for, including ones that are # well outside the automatic view limits. fig, axs = plt.subplots(2, 1, figsize=(5, 3), tight_layout=True) @@ -283,7 +281,7 @@ plt.show() # %% -# Tick Locators and Formatters +# Tick locators and formatters # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # # Instead of making a list of all the ticklabels, we could have @@ -317,14 +315,14 @@ # %% # The default formatter is the `matplotlib.ticker.MaxNLocator` called as -# ``ticker.MaxNLocator(self, nbins='auto', steps=[1, 2, 2.5, 5, 10])`` -# The *steps* keyword contains a list of multiples that can be used for -# tick values. i.e. in this case, 2, 4, 6 would be acceptable ticks, +# ``ticker.MaxNLocator(self, nbins='auto', steps=[1, 2, 2.5, 5, 10])``. +# The ``steps`` argument contains a list of multiples that can be used for +# tick values. In this case, 2, 4, 6 would be acceptable ticks, # as would 20, 40, 60 or 0.2, 0.4, 0.6. However, 3, 6, 9 would not be # acceptable because 3 doesn't appear in the list of steps. # -# ``nbins=auto`` uses an algorithm to determine how many ticks will -# be acceptable based on how long the axis is. The fontsize of the +# Setting ``nbins=auto`` uses an algorithm to determine how many ticks will +# be acceptable based on the axis length. The fontsize of the # ticklabel is taken into account, but the length of the tick string # is not (because it's not yet known.) In the bottom row, the # ticklabels are quite large, so we set ``nbins=4`` to make the @@ -382,11 +380,11 @@ def formatoddticks(x, pos): # Matplotlib can accept `datetime.datetime` and `numpy.datetime64` # objects as plotting arguments. Dates and times require special # formatting, which can often benefit from manual intervention. In -# order to help, dates have special Locators and Formatters, +# order to help, dates have special locators and formatters, # defined in the `matplotlib.dates` module. # -# A simple example is as follows. Note how we have to rotate the -# tick labels so that they don't over-run each other. +# The following simple example illustrates this concept. Note how we +# rotate the tick labels so that they don't overlap. import datetime @@ -399,11 +397,10 @@ def formatoddticks(x, pos): plt.show() # %% -# We can pass a format to `matplotlib.dates.DateFormatter`. Also note that the -# 29th and the next month are very close together. We can fix this by using -# the `.dates.DayLocator` class, which allows us to specify a list of days of -# the month to use. Similar formatters are listed in the `matplotlib.dates` -# module. +# We can pass a format to `matplotlib.dates.DateFormatter`. If two tick labels +# are very close together, we can use the `.dates.DayLocator` class, which +# allows us to specify a list of days of the month to use. Similar formatters +# are listed in the `matplotlib.dates` module. import matplotlib.dates as mdates @@ -418,9 +415,9 @@ def formatoddticks(x, pos): plt.show() # %% -# Legends and Annotations +# Legends and annotations # ======================= # -# - Legends: :ref:`legend_guide` -# - Annotations: :ref:`annotations` +# - :ref:`legend_guide` +# - :ref:`annotations` # diff --git a/galleries/users_explain/toolkits/mplot3d.rst b/galleries/users_explain/toolkits/mplot3d.rst index 2551c065ea46..b4ddc48790cb 100644 --- a/galleries/users_explain/toolkits/mplot3d.rst +++ b/galleries/users_explain/toolkits/mplot3d.rst @@ -111,6 +111,18 @@ See `.Axes3D.contourf` for API documentation. The feature demoed in the second contourf3d example was enabled as a result of a bugfix for version 1.1.0. +.. _fillbetween3d: + +Fill between 3D lines +===================== +See `.Axes3D.fill_between` for API documentation. + +.. figure:: /gallery/mplot3d/images/sphx_glr_fillbetween3d_001.png + :target: /gallery/mplot3d/fillbetween3d.html + :align: center + +.. versionadded:: 3.10 + .. _polygon3d: Polygon plots diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index ad4676b11ae0..5f964e0b34de 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -129,6 +129,8 @@ "interactive", "is_interactive", "colormaps", + "multivar_colormaps", + "bivar_colormaps", "color_sequences", ] @@ -157,10 +159,8 @@ # 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 matplotlib.cbook import sanitize_sequence from matplotlib._api import MatplotlibDeprecationWarning from matplotlib.rcsetup import cycler # noqa: F401 -from matplotlib.rcsetup import validate_backend _log = logging.getLogger(__name__) @@ -225,6 +225,7 @@ def _get_version(): else: return setuptools_scm.get_version( root=root, + dist_name="matplotlib", version_scheme="release-branch-semver", local_scheme="node-and-date", fallback_version=_version.version, @@ -715,6 +716,35 @@ def _get(self, key): """ return dict.__getitem__(self, key) + def _update_raw(self, other_params): + """ + Directly update the data from *other_params*, bypassing deprecation, + backend and validation logic on both sides. + + This ``rcParams._update_raw(params)`` replaces the previous pattern + ``dict.update(rcParams, params)``. + + Parameters + ---------- + other_params : dict or `.RcParams` + The input mapping from which to update. + """ + if isinstance(other_params, RcParams): + other_params = dict.items(other_params) + dict.update(self, other_params) + + def _ensure_has_backend(self): + """ + Ensure that a "backend" entry exists. + + Normally, the default matplotlibrc file contains *no* entry for "backend" (the + corresponding line starts with ##, not #; we fill in _auto_backend_sentinel + in that case. However, packagers can set a different default backend + (resulting in a normal `#backend: foo` line) in which case we should *not* + fill in _auto_backend_sentinel. + """ + dict.setdefault(self, "backend", rcsetup._auto_backend_sentinel) + def __setitem__(self, key, val): try: if key in _deprecated_map: @@ -964,24 +994,17 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): return config -# When constructing the global instances, we need to perform certain updates -# by explicitly calling the superclass (dict.update, dict.items) to avoid -# triggering resolution of _auto_backend_sentinel. rcParamsDefault = _rc_params_in_file( cbook._get_data_path("matplotlibrc"), # Strip leading comment. transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) -dict.update(rcParamsDefault, rcsetup._hardcoded_defaults) -# Normally, the default matplotlibrc file contains *no* entry for backend (the -# corresponding line starts with ##, not #; we fill on _auto_backend_sentinel -# in that case. However, packagers can set a different default backend -# (resulting in a normal `#backend: foo` line) in which case we should *not* -# fill in _auto_backend_sentinel. -dict.setdefault(rcParamsDefault, "backend", rcsetup._auto_backend_sentinel) +rcParamsDefault._update_raw(rcsetup._hardcoded_defaults) +rcParamsDefault._ensure_has_backend() + rcParams = RcParams() # The global instance. -dict.update(rcParams, dict.items(rcParamsDefault)) -dict.update(rcParams, _rc_params_in_file(matplotlib_fname())) +rcParams._update_raw(rcParamsDefault) +rcParams._update_raw(_rc_params_in_file(matplotlib_fname())) rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. @@ -1193,7 +1216,7 @@ def rc_context(rc=None, fname=None): rcParams.update(rc) yield finally: - dict.update(rcParams, orig) # Revert to the original rcs. + rcParams._update_raw(orig) # Revert to the original rcs. def use(backend, *, force=True): @@ -1239,7 +1262,7 @@ def use(backend, *, force=True): matplotlib.pyplot.switch_backend """ - name = validate_backend(backend) + name = rcsetup.validate_backend(backend) # don't (prematurely) resolve the "auto" backend setting if rcParams._get_backend_or_none() == name: # Nothing to do if the requested backend is already set @@ -1273,15 +1296,37 @@ def use(backend, *, force=True): rcParams['backend'] = os.environ.get('MPLBACKEND') -def get_backend(): +def get_backend(*, auto_select=True): """ Return the name of the current backend. + Parameters + ---------- + auto_select : bool, default: True + Whether to trigger backend resolution if no backend has been + selected so far. If True, this ensures that a valid backend + is returned. If False, this returns None if no backend has been + selected so far. + + .. versionadded:: 3.10 + + .. admonition:: Provisional + + The *auto_select* flag is provisional. It may be changed or removed + without prior warning. + See Also -------- matplotlib.use """ - return rcParams['backend'] + if auto_select: + return rcParams['backend'] + else: + backend = rcParams._get('backend') + if backend is rcsetup._auto_backend_sentinel: + return None + else: + return backend def interactive(b): @@ -1343,7 +1388,7 @@ def _replacer(data, value): except Exception: # key does not exist, silently fall back to key pass - return sanitize_sequence(value) + return cbook.sanitize_sequence(value) def _label_from_arg(y, default_name): @@ -1380,10 +1425,10 @@ def _add_data_doc(docstring, replace_names): data_doc = ("""\ If given, all parameters also accept a string ``s``, which is - interpreted as ``data[s]`` (unless this raises an exception).""" + interpreted as ``data[s]`` if ``s`` is a key in ``data``.""" if replace_names is None else f"""\ If given, the following parameters also accept a string ``s``, which is - interpreted as ``data[s]`` (unless this raises an exception): + interpreted as ``data[s]`` if ``s`` is a key in ``data``: {', '.join(map('*{}*'.format, replace_names))}""") # using string replacement instead of formatting has the advantages @@ -1475,8 +1520,8 @@ def inner(ax, *args, data=None, **kwargs): if data is None: return func( ax, - *map(sanitize_sequence, args), - **{k: sanitize_sequence(v) for k, v in kwargs.items()}) + *map(cbook.sanitize_sequence, args), + **{k: cbook.sanitize_sequence(v) for k, v in kwargs.items()}) bound = new_sig.bind(ax, *args, **kwargs) auto_label = (bound.arguments.get(label_namer) @@ -1513,7 +1558,19 @@ def inner(ax, *args, data=None, **kwargs): _log.debug('platform is %s', sys.platform) +@_api.deprecated("3.10", alternative="matplotlib.cbook.sanitize_sequence") +def sanitize_sequence(data): + return cbook.sanitize_sequence(data) + + +@_api.deprecated("3.10", alternative="matplotlib.rcsetup.validate_backend") +def validate_backend(s): + return rcsetup.validate_backend(s) + + # workaround: we must defer colormaps import to after loading rcParams, because # colormap creation depends on rcParams from matplotlib.cm import _colormaps as colormaps # noqa: E402 +from matplotlib.cm import _multivar_colormaps as multivar_colormaps # noqa: E402 +from matplotlib.cm import _bivar_colormaps as bivar_colormaps # noqa: E402 from matplotlib.colors import _color_sequences as color_sequences # noqa: E402 diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 54b28a8318ef..88058ffd7def 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -37,7 +37,7 @@ import contextlib from packaging.version import Version from matplotlib._api import MatplotlibDeprecationWarning -from typing import Any, NamedTuple +from typing import Any, Literal, NamedTuple, overload class _VersionInfo(NamedTuple): major: int @@ -70,6 +70,10 @@ class RcParams(dict[str, Any]): def __init__(self, *args, **kwargs) -> None: ... def _set(self, key: str, val: Any) -> None: ... def _get(self, key: str) -> 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]: ... @@ -100,7 +104,10 @@ def rc_context( rc: dict[str, Any] | None = ..., fname: str | Path | os.PathLike | None = ... ) -> Generator[None, None, None]: ... def use(backend: str, *, force: bool = ...) -> None: ... -def get_backend() -> str: ... +@overload +def get_backend(*, auto_select: Literal[True] = True) -> str: ... +@overload +def get_backend(*, auto_select: Literal[False]) -> str | None: ... def interactive(b: bool) -> None: ... def is_interactive() -> bool: ... @@ -111,5 +118,7 @@ def _preprocess_data( label_namer: str | None = ... ) -> Callable: ... -from matplotlib.cm import _colormaps as colormaps -from matplotlib.colors import _color_sequences as color_sequences +from matplotlib.cm import _colormaps as colormaps # noqa: E402 +from matplotlib.cm import _multivar_colormaps as multivar_colormaps # noqa: E402 +from matplotlib.cm import _bivar_colormaps as bivar_colormaps # noqa: E402 +from matplotlib.colors import _color_sequences as color_sequences # noqa: E402 diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 27d68529b7d4..22b58b62ff8e 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -12,6 +12,7 @@ import functools import itertools +import pathlib import re import sys import warnings @@ -366,16 +367,25 @@ def warn_external(message, category=None): warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, etc.). """ - frame = sys._getframe() - for stacklevel in itertools.count(1): - if frame is None: - # when called in embedded context may hit frame is None - break - if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", - # Work around sphinx-gallery not setting __name__. - frame.f_globals.get("__name__", "")): - break - frame = frame.f_back - # preemptively break reference cycle between locals and the frame - del frame - warnings.warn(message, category, stacklevel) + kwargs = {} + if sys.version_info[:2] >= (3, 12): + # Go to Python's `site-packages` or `lib` from an editable install. + basedir = pathlib.Path(__file__).parents[2] + kwargs['skip_file_prefixes'] = (str(basedir / 'matplotlib'), + str(basedir / 'mpl_toolkits')) + else: + frame = sys._getframe() + for stacklevel in itertools.count(1): + if frame is None: + # when called in embedded context may hit frame is None + kwargs['stacklevel'] = stacklevel + break + if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", + # Work around sphinx-gallery not setting __name__. + frame.f_globals.get("__name__", "")): + kwargs['stacklevel'] = stacklevel + break + frame = frame.f_back + # preemptively break reference cycle between locals and the frame + del frame + warnings.warn(message, category, **kwargs) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 4baff7cd804c..ea7076feac3c 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -1,9 +1,10 @@ -from collections.abc import Callable, Generator, Mapping, Sequence -from typing import Any, Iterable, TypeVar, overload +from collections.abc import Callable, Generator, Iterable, Mapping, Sequence +from typing import Any, TypeVar, overload +from typing_extensions import Self # < Py 3.11 from numpy.typing import NDArray -from .deprecation import ( # noqa: re-exported API +from .deprecation import ( # noqa: F401, re-exported API deprecated as deprecated, warn_deprecated as warn_deprecated, rename_parameter as rename_parameter, @@ -25,9 +26,8 @@ class classproperty(Any): fdel: None = ..., doc: str | None = None, ): ... - # Replace return with Self when py3.9 is dropped @overload - def __get__(self, instance: None, owner: None) -> classproperty: ... + def __get__(self, instance: None, owner: None) -> Self: ... @overload def __get__(self, instance: object, owner: type[object]) -> Any: ... @property diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 283a55f1beb0..65a754bbb43d 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -26,25 +26,20 @@ def _generate_deprecation_warning( addendum='', *, removal=''): if pending: if removal: - raise ValueError( - "A pending deprecation cannot have a scheduled removal") - else: - if not removal: - macro, meso, *_ = since.split('.') - removal = f'{macro}.{int(meso) + 2}' - removal = f"in {removal}" + raise ValueError("A pending deprecation cannot have a scheduled removal") + elif removal == '': + macro, meso, *_ = since.split('.') + removal = f'{macro}.{int(meso) + 2}' if not message: message = ( - ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") - + (" will be deprecated in a future version" - if pending else - " was deprecated in Matplotlib %(since)s and will be removed %(removal)s" - ) - + "." - + (" Use %(alternative)s instead." if alternative else "") - + (" %(addendum)s" if addendum else "")) - warning_cls = (PendingDeprecationWarning if pending - else MatplotlibDeprecationWarning) + ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") + + (" will be deprecated in a future version" if pending else + (" was deprecated in Matplotlib %(since)s" + + (" and will be removed in %(removal)s" if removal else ""))) + + "." + + (" Use %(alternative)s instead." if alternative else "") + + (" %(addendum)s" if addendum else "")) + warning_cls = PendingDeprecationWarning if pending else MatplotlibDeprecationWarning return warning_cls(message % dict( func=name, name=name, obj_type=obj_type, since=since, removal=removal, alternative=alternative, addendum=addendum)) @@ -295,7 +290,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message=f"The {old!r} parameter of {func.__name__}() " f"has been renamed {new!r} since Matplotlib {since}; support " - f"for the old name will be dropped %(removal)s.") + f"for the old name will be dropped in %(removal)s.") kwargs[new] = kwargs.pop(old) return func(*args, **kwargs) @@ -390,12 +385,12 @@ def wrapper(*inner_args, **inner_kwargs): warn_deprecated( since, message=f"Additional positional arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") elif is_varkwargs and arguments.get(name): warn_deprecated( since, message=f"Additional keyword arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") # We cannot just check `name not in arguments` because the pyplot # wrappers always pass all arguments explicitly. elif any(name in d and d[name] != _deprecated_parameter @@ -437,7 +432,8 @@ def make_keyword_only(since, name, func=None): assert (name in signature.parameters and signature.parameters[name].kind == POK), ( f"Matplotlib internal error: {name!r} must be a positional-or-keyword " - f"parameter for {func.__name__}()") + f"parameter for {func.__name__}(). If this error happens on a function with a " + f"pyplot wrapper, make sure make_keyword_only() is the outermost decorator.") names = [*signature.parameters] name_idx = names.index(name) kwonly = [name for name in names[name_idx:] @@ -452,7 +448,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message="Passing the %(name)s %(obj_type)s " "positionally is deprecated since Matplotlib %(since)s; the " - "parameter will become keyword-only %(removal)s.", + "parameter will become keyword-only in %(removal)s.", name=name, obj_type=f"parameter of {func.__name__}()") return func(*args, **kwargs) diff --git a/lib/matplotlib/_api/deprecation.pyi b/lib/matplotlib/_api/deprecation.pyi index 9619d1b484fc..e050290662d9 100644 --- a/lib/matplotlib/_api/deprecation.pyi +++ b/lib/matplotlib/_api/deprecation.pyi @@ -1,8 +1,7 @@ from collections.abc import Callable import contextlib -from typing import Any, TypedDict, TypeVar, overload +from typing import Any, Literal, ParamSpec, TypedDict, TypeVar, overload from typing_extensions import ( - ParamSpec, # < Py 3.10 Unpack, # < Py 3.11 ) @@ -18,7 +17,7 @@ class DeprecationKwargs(TypedDict, total=False): pending: bool obj_type: str addendum: str - removal: str + removal: str | Literal[False] class NamedDeprecationKwargs(DeprecationKwargs, total=False): name: str diff --git a/lib/matplotlib/_cm.py b/lib/matplotlib/_cm.py index 59d260107f3b..b942d1697934 100644 --- a/lib/matplotlib/_cm.py +++ b/lib/matplotlib/_cm.py @@ -1366,6 +1366,20 @@ def _gist_yarg(x): return 1 - x ) +_petroff10_data = ( + (0.24705882352941178, 0.5647058823529412, 0.8549019607843137), # 3f90da + (1.0, 0.6627450980392157, 0.054901960784313725), # ffa90e + (0.7411764705882353, 0.12156862745098039, 0.00392156862745098), # bd1f01 + (0.5803921568627451, 0.6431372549019608, 0.6352941176470588), # 94a4a2 + (0.5137254901960784, 0.17647058823529413, 0.7137254901960784), # 832db6 + (0.6627450980392157, 0.4196078431372549, 0.34901960784313724), # a96b59 + (0.9058823529411765, 0.38823529411764707, 0.0), # e76300 + (0.7254901960784313, 0.6745098039215687, 0.4392156862745098), # b9ac70 + (0.44313725490196076, 0.4588235294117647, 0.5058823529411764), # 717581 + (0.5725490196078431, 0.8549019607843137, 0.8666666666666667), # 92dadd +) + + datad = { 'Blues': _Blues_data, 'BrBG': _BrBG_data, diff --git a/lib/matplotlib/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py new file mode 100644 index 000000000000..53c0d48d7d6c --- /dev/null +++ b/lib/matplotlib/_cm_bivar.py @@ -0,0 +1,1312 @@ +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-24 + +import numpy as np +from matplotlib.colors import SegmentedBivarColormap + +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, + 0.882, 0.000, 0.711, 0.875, 0.000, 0.715, 0.867, 0.000, 0.720, 0.860, + 0.000, 0.725, 0.853, 0.000, 0.729, 0.845, 0.000, 0.733, 0.838, 0.000, + 0.737, 0.831, 0.000, 0.741, 0.824, 0.000, 0.745, 0.816, 0.000, 0.749, + 0.809, 0.000, 0.752, 0.802, 0.000, 0.756, 0.794, 0.000, 0.759, 0.787, + 0.000, 0.762, 0.779, 0.000, 0.765, 0.771, 0.000, 0.767, 0.764, 0.000, + 0.770, 0.755, 0.000, 0.772, 0.747, 0.000, 0.774, 0.739, 0.000, 0.776, + 0.730, 0.000, 0.777, 0.721, 0.000, 0.779, 0.712, 0.021, 0.780, 0.702, + 0.055, 0.781, 0.693, 0.079, 0.782, 0.682, 0.097, 0.782, 0.672, 0.111, + 0.782, 0.661, 0.122, 0.782, 0.650, 0.132, 0.782, 0.639, 0.140, 0.781, + 0.627, 0.147, 0.781, 0.615, 0.154, 0.780, 0.602, 0.159, 0.778, 0.589, + 0.164, 0.777, 0.576, 0.169, 0.775, 0.563, 0.173, 0.773, 0.549, 0.177, + 0.771, 0.535, 0.180, 0.768, 0.520, 0.184, 0.766, 0.505, 0.187, 0.763, + 0.490, 0.190, 0.760, 0.474, 0.193, 0.756, 0.458, 0.196, 0.753, 0.442, + 0.200, 0.749, 0.425, 0.203, 0.745, 0.408, 0.206, 0.741, 0.391, 0.210, + 0.736, 0.373, 0.213, 0.732, 0.355, 0.216, 0.727, 0.337, 0.220, 0.722, + 0.318, 0.224, 0.717, 0.298, 0.227, 0.712, 0.278, 0.231, 0.707, 0.258, + 0.235, 0.701, 0.236, 0.239, 0.696, 0.214, 0.242, 0.690, 0.190, 0.246, + 0.684, 0.165, 0.250, 0.678, 0.136, 0.000, 0.675, 0.934, 0.000, 0.681, + 0.925, 0.000, 0.687, 0.917, 0.000, 0.692, 0.909, 0.000, 0.697, 0.901, + 0.000, 0.703, 0.894, 0.000, 0.708, 0.886, 0.000, 0.713, 0.879, 0.000, + 0.718, 0.872, 0.000, 0.722, 0.864, 0.000, 0.727, 0.857, 0.000, 0.731, + 0.850, 0.000, 0.736, 0.843, 0.000, 0.740, 0.836, 0.000, 0.744, 0.829, + 0.000, 0.748, 0.822, 0.000, 0.752, 0.815, 0.000, 0.755, 0.808, 0.000, + 0.759, 0.800, 0.000, 0.762, 0.793, 0.000, 0.765, 0.786, 0.000, 0.768, + 0.778, 0.000, 0.771, 0.770, 0.000, 0.773, 0.762, 0.051, 0.776, 0.754, + 0.087, 0.778, 0.746, 0.111, 0.780, 0.737, 0.131, 0.782, 0.728, 0.146, + 0.783, 0.719, 0.159, 0.784, 0.710, 0.171, 0.785, 0.700, 0.180, 0.786, + 0.690, 0.189, 0.786, 0.680, 0.196, 0.787, 0.669, 0.202, 0.787, 0.658, + 0.208, 0.786, 0.647, 0.213, 0.786, 0.635, 0.217, 0.785, 0.623, 0.221, + 0.784, 0.610, 0.224, 0.782, 0.597, 0.227, 0.781, 0.584, 0.230, 0.779, + 0.570, 0.232, 0.777, 0.556, 0.234, 0.775, 0.542, 0.236, 0.772, 0.527, + 0.238, 0.769, 0.512, 0.240, 0.766, 0.497, 0.242, 0.763, 0.481, 0.244, + 0.760, 0.465, 0.246, 0.756, 0.448, 0.248, 0.752, 0.432, 0.250, 0.748, + 0.415, 0.252, 0.744, 0.397, 0.254, 0.739, 0.379, 0.256, 0.735, 0.361, + 0.259, 0.730, 0.343, 0.261, 0.725, 0.324, 0.264, 0.720, 0.304, 0.266, + 0.715, 0.284, 0.269, 0.709, 0.263, 0.271, 0.704, 0.242, 0.274, 0.698, + 0.220, 0.277, 0.692, 0.196, 0.280, 0.686, 0.170, 0.283, 0.680, 0.143, + 0.000, 0.676, 0.937, 0.000, 0.682, 0.928, 0.000, 0.688, 0.920, 0.000, + 0.694, 0.913, 0.000, 0.699, 0.905, 0.000, 0.704, 0.897, 0.000, 0.710, + 0.890, 0.000, 0.715, 0.883, 0.000, 0.720, 0.876, 0.000, 0.724, 0.869, + 0.000, 0.729, 0.862, 0.000, 0.734, 0.855, 0.000, 0.738, 0.848, 0.000, + 0.743, 0.841, 0.000, 0.747, 0.834, 0.000, 0.751, 0.827, 0.000, 0.755, + 0.820, 0.000, 0.759, 0.813, 0.000, 0.762, 0.806, 0.003, 0.766, 0.799, + 0.066, 0.769, 0.792, 0.104, 0.772, 0.784, 0.131, 0.775, 0.777, 0.152, + 0.777, 0.769, 0.170, 0.780, 0.761, 0.185, 0.782, 0.753, 0.198, 0.784, + 0.744, 0.209, 0.786, 0.736, 0.219, 0.787, 0.727, 0.228, 0.788, 0.717, + 0.236, 0.789, 0.708, 0.243, 0.790, 0.698, 0.249, 0.791, 0.688, 0.254, + 0.791, 0.677, 0.259, 0.791, 0.666, 0.263, 0.791, 0.654, 0.266, 0.790, + 0.643, 0.269, 0.789, 0.631, 0.272, 0.788, 0.618, 0.274, 0.787, 0.605, + 0.276, 0.785, 0.592, 0.278, 0.783, 0.578, 0.279, 0.781, 0.564, 0.280, + 0.779, 0.549, 0.282, 0.776, 0.535, 0.283, 0.773, 0.519, 0.284, 0.770, + 0.504, 0.285, 0.767, 0.488, 0.286, 0.763, 0.472, 0.287, 0.759, 0.455, + 0.288, 0.756, 0.438, 0.289, 0.751, 0.421, 0.291, 0.747, 0.403, 0.292, + 0.742, 0.385, 0.293, 0.738, 0.367, 0.295, 0.733, 0.348, 0.296, 0.728, + 0.329, 0.298, 0.723, 0.310, 0.300, 0.717, 0.290, 0.302, 0.712, 0.269, + 0.304, 0.706, 0.247, 0.306, 0.700, 0.225, 0.308, 0.694, 0.201, 0.310, + 0.688, 0.176, 0.312, 0.682, 0.149, 0.000, 0.678, 0.939, 0.000, 0.683, + 0.931, 0.000, 0.689, 0.923, 0.000, 0.695, 0.916, 0.000, 0.701, 0.908, + 0.000, 0.706, 0.901, 0.000, 0.711, 0.894, 0.000, 0.717, 0.887, 0.000, + 0.722, 0.880, 0.000, 0.727, 0.873, 0.000, 0.732, 0.866, 0.000, 0.736, + 0.859, 0.000, 0.741, 0.853, 0.000, 0.745, 0.846, 0.000, 0.750, 0.839, + 0.000, 0.754, 0.833, 0.035, 0.758, 0.826, 0.091, 0.762, 0.819, 0.126, + 0.765, 0.812, 0.153, 0.769, 0.805, 0.174, 0.772, 0.798, 0.193, 0.775, + 0.791, 0.209, 0.778, 0.783, 0.223, 0.781, 0.776, 0.236, 0.784, 0.768, + 0.247, 0.786, 0.760, 0.257, 0.788, 0.752, 0.266, 0.790, 0.743, 0.273, + 0.791, 0.734, 0.280, 0.793, 0.725, 0.287, 0.794, 0.715, 0.292, 0.794, + 0.706, 0.297, 0.795, 0.695, 0.301, 0.795, 0.685, 0.305, 0.795, 0.674, + 0.308, 0.795, 0.662, 0.310, 0.794, 0.651, 0.312, 0.794, 0.638, 0.314, + 0.792, 0.626, 0.316, 0.791, 0.613, 0.317, 0.789, 0.599, 0.318, 0.787, + 0.586, 0.319, 0.785, 0.571, 0.320, 0.783, 0.557, 0.320, 0.780, 0.542, + 0.321, 0.777, 0.527, 0.321, 0.774, 0.511, 0.322, 0.770, 0.495, 0.322, + 0.767, 0.478, 0.323, 0.763, 0.462, 0.323, 0.759, 0.445, 0.324, 0.755, + 0.427, 0.325, 0.750, 0.410, 0.325, 0.745, 0.391, 0.326, 0.741, 0.373, + 0.327, 0.736, 0.354, 0.328, 0.730, 0.335, 0.329, 0.725, 0.315, 0.330, + 0.720, 0.295, 0.331, 0.714, 0.274, 0.333, 0.708, 0.253, 0.334, 0.702, + 0.230, 0.336, 0.696, 0.207, 0.337, 0.690, 0.182, 0.339, 0.684, 0.154, + 0.000, 0.679, 0.942, 0.000, 0.685, 0.934, 0.000, 0.691, 0.927, 0.000, + 0.696, 0.919, 0.000, 0.702, 0.912, 0.000, 0.708, 0.905, 0.000, 0.713, + 0.898, 0.000, 0.718, 0.891, 0.000, 0.724, 0.884, 0.000, 0.729, 0.877, + 0.000, 0.734, 0.871, 0.000, 0.739, 0.864, 0.000, 0.743, 0.857, 0.035, + 0.748, 0.851, 0.096, 0.752, 0.844, 0.133, 0.757, 0.838, 0.161, 0.761, + 0.831, 0.185, 0.765, 0.825, 0.205, 0.769, 0.818, 0.223, 0.772, 0.811, + 0.238, 0.776, 0.804, 0.252, 0.779, 0.797, 0.265, 0.782, 0.790, 0.276, + 0.785, 0.783, 0.286, 0.788, 0.775, 0.296, 0.790, 0.767, 0.304, 0.792, + 0.759, 0.311, 0.794, 0.751, 0.318, 0.796, 0.742, 0.324, 0.797, 0.733, + 0.329, 0.798, 0.723, 0.334, 0.799, 0.714, 0.338, 0.799, 0.703, 0.341, + 0.800, 0.693, 0.344, 0.800, 0.682, 0.347, 0.799, 0.670, 0.349, 0.799, + 0.659, 0.351, 0.798, 0.646, 0.352, 0.797, 0.634, 0.353, 0.795, 0.621, + 0.354, 0.794, 0.607, 0.354, 0.792, 0.593, 0.355, 0.789, 0.579, 0.355, + 0.787, 0.564, 0.355, 0.784, 0.549, 0.355, 0.781, 0.534, 0.355, 0.778, + 0.518, 0.355, 0.774, 0.502, 0.355, 0.770, 0.485, 0.355, 0.766, 0.468, + 0.355, 0.762, 0.451, 0.355, 0.758, 0.434, 0.355, 0.753, 0.416, 0.356, + 0.748, 0.397, 0.356, 0.743, 0.379, 0.356, 0.738, 0.360, 0.357, 0.733, + 0.340, 0.357, 0.728, 0.321, 0.358, 0.722, 0.300, 0.359, 0.716, 0.279, + 0.360, 0.710, 0.258, 0.361, 0.704, 0.235, 0.361, 0.698, 0.212, 0.362, + 0.692, 0.187, 0.363, 0.686, 0.160, 0.000, 0.680, 0.945, 0.000, 0.686, + 0.937, 0.000, 0.692, 0.930, 0.000, 0.698, 0.922, 0.000, 0.703, 0.915, + 0.000, 0.709, 0.908, 0.000, 0.715, 0.901, 0.000, 0.720, 0.894, 0.000, + 0.726, 0.888, 0.000, 0.731, 0.881, 0.007, 0.736, 0.875, 0.084, 0.741, + 0.869, 0.127, 0.746, 0.862, 0.159, 0.751, 0.856, 0.185, 0.755, 0.850, + 0.208, 0.760, 0.843, 0.227, 0.764, 0.837, 0.245, 0.768, 0.830, 0.260, + 0.772, 0.824, 0.275, 0.776, 0.817, 0.288, 0.779, 0.811, 0.300, 0.783, + 0.804, 0.310, 0.786, 0.797, 0.320, 0.789, 0.789, 0.329, 0.792, 0.782, + 0.337, 0.794, 0.774, 0.345, 0.796, 0.766, 0.351, 0.798, 0.758, 0.357, + 0.800, 0.749, 0.363, 0.801, 0.740, 0.367, 0.803, 0.731, 0.371, 0.803, + 0.721, 0.375, 0.804, 0.711, 0.378, 0.804, 0.701, 0.380, 0.804, 0.690, + 0.382, 0.804, 0.679, 0.384, 0.803, 0.667, 0.385, 0.802, 0.654, 0.386, + 0.801, 0.642, 0.386, 0.800, 0.629, 0.387, 0.798, 0.615, 0.387, 0.796, + 0.601, 0.387, 0.793, 0.587, 0.387, 0.791, 0.572, 0.387, 0.788, 0.557, + 0.386, 0.785, 0.541, 0.386, 0.781, 0.525, 0.385, 0.778, 0.509, 0.385, + 0.774, 0.492, 0.385, 0.770, 0.475, 0.384, 0.765, 0.457, 0.384, 0.761, + 0.440, 0.384, 0.756, 0.422, 0.384, 0.751, 0.403, 0.384, 0.746, 0.384, + 0.384, 0.741, 0.365, 0.384, 0.735, 0.346, 0.384, 0.730, 0.326, 0.384, + 0.724, 0.305, 0.384, 0.718, 0.284, 0.385, 0.712, 0.263, 0.385, 0.706, + 0.240, 0.386, 0.700, 0.217, 0.386, 0.694, 0.192, 0.387, 0.687, 0.165, + 0.000, 0.680, 0.948, 0.000, 0.687, 0.940, 0.000, 0.693, 0.933, 0.000, + 0.699, 0.925, 0.000, 0.705, 0.918, 0.000, 0.711, 0.912, 0.000, 0.716, + 0.905, 0.000, 0.722, 0.898, 0.050, 0.728, 0.892, 0.109, 0.733, 0.886, + 0.147, 0.738, 0.879, 0.177, 0.743, 0.873, 0.202, 0.748, 0.867, 0.224, + 0.753, 0.861, 0.243, 0.758, 0.855, 0.261, 0.763, 0.849, 0.277, 0.767, + 0.842, 0.292, 0.771, 0.836, 0.305, 0.775, 0.830, 0.318, 0.779, 0.823, + 0.329, 0.783, 0.817, 0.340, 0.787, 0.810, 0.350, 0.790, 0.803, 0.359, + 0.793, 0.796, 0.367, 0.796, 0.789, 0.374, 0.798, 0.782, 0.381, 0.801, + 0.774, 0.387, 0.803, 0.766, 0.393, 0.804, 0.757, 0.397, 0.806, 0.748, + 0.402, 0.807, 0.739, 0.405, 0.808, 0.729, 0.408, 0.809, 0.719, 0.411, + 0.809, 0.709, 0.413, 0.809, 0.698, 0.415, 0.808, 0.687, 0.416, 0.808, + 0.675, 0.417, 0.807, 0.663, 0.417, 0.806, 0.650, 0.417, 0.804, 0.637, + 0.418, 0.802, 0.623, 0.417, 0.800, 0.609, 0.417, 0.798, 0.594, 0.416, + 0.795, 0.579, 0.416, 0.792, 0.564, 0.415, 0.789, 0.548, 0.414, 0.785, + 0.532, 0.414, 0.781, 0.515, 0.413, 0.777, 0.499, 0.412, 0.773, 0.481, + 0.412, 0.769, 0.464, 0.411, 0.764, 0.446, 0.410, 0.759, 0.428, 0.410, + 0.754, 0.409, 0.409, 0.749, 0.390, 0.409, 0.743, 0.371, 0.409, 0.738, + 0.351, 0.409, 0.732, 0.331, 0.408, 0.726, 0.310, 0.408, 0.720, 0.289, + 0.408, 0.714, 0.268, 0.408, 0.708, 0.245, 0.409, 0.702, 0.222, 0.409, + 0.695, 0.197, 0.409, 0.689, 0.170, 0.000, 0.681, 0.950, 0.000, 0.688, + 0.943, 0.000, 0.694, 0.936, 0.000, 0.700, 0.929, 0.000, 0.706, 0.922, + 0.000, 0.712, 0.915, 0.074, 0.718, 0.908, 0.124, 0.724, 0.902, 0.159, + 0.730, 0.896, 0.188, 0.735, 0.890, 0.213, 0.740, 0.884, 0.235, 0.746, + 0.878, 0.255, 0.751, 0.872, 0.273, 0.756, 0.866, 0.289, 0.761, 0.860, + 0.305, 0.766, 0.854, 0.319, 0.770, 0.848, 0.332, 0.775, 0.842, 0.344, + 0.779, 0.836, 0.356, 0.783, 0.830, 0.366, 0.787, 0.823, 0.376, 0.790, + 0.817, 0.385, 0.794, 0.810, 0.394, 0.797, 0.803, 0.401, 0.800, 0.796, + 0.408, 0.802, 0.789, 0.414, 0.805, 0.781, 0.420, 0.807, 0.773, 0.425, + 0.809, 0.765, 0.430, 0.810, 0.756, 0.433, 0.812, 0.747, 0.437, 0.813, + 0.738, 0.440, 0.813, 0.728, 0.442, 0.814, 0.717, 0.444, 0.813, 0.706, + 0.445, 0.813, 0.695, 0.446, 0.812, 0.683, 0.446, 0.811, 0.671, 0.447, + 0.810, 0.658, 0.447, 0.809, 0.645, 0.446, 0.807, 0.631, 0.446, 0.804, + 0.617, 0.445, 0.802, 0.602, 0.444, 0.799, 0.587, 0.443, 0.796, 0.571, + 0.442, 0.792, 0.555, 0.441, 0.789, 0.539, 0.440, 0.785, 0.522, 0.439, + 0.781, 0.505, 0.438, 0.776, 0.488, 0.437, 0.772, 0.470, 0.436, 0.767, + 0.452, 0.435, 0.762, 0.433, 0.435, 0.757, 0.415, 0.434, 0.751, 0.396, + 0.433, 0.746, 0.376, 0.432, 0.740, 0.356, 0.432, 0.734, 0.336, 0.431, + 0.728, 0.315, 0.431, 0.722, 0.294, 0.431, 0.716, 0.272, 0.430, 0.710, + 0.250, 0.430, 0.703, 0.226, 0.430, 0.697, 0.201, 0.430, 0.690, 0.175, + 0.000, 0.682, 0.953, 0.000, 0.689, 0.946, 0.000, 0.695, 0.938, 0.002, + 0.701, 0.932, 0.086, 0.708, 0.925, 0.133, 0.714, 0.918, 0.167, 0.720, + 0.912, 0.196, 0.726, 0.906, 0.221, 0.731, 0.900, 0.243, 0.737, 0.894, + 0.263, 0.743, 0.888, 0.281, 0.748, 0.882, 0.298, 0.753, 0.876, 0.314, + 0.759, 0.870, 0.329, 0.764, 0.865, 0.342, 0.768, 0.859, 0.355, 0.773, + 0.853, 0.368, 0.778, 0.847, 0.379, 0.782, 0.842, 0.390, 0.786, 0.836, + 0.400, 0.790, 0.830, 0.409, 0.794, 0.823, 0.417, 0.798, 0.817, 0.425, + 0.801, 0.810, 0.433, 0.804, 0.803, 0.439, 0.807, 0.796, 0.445, 0.809, + 0.789, 0.451, 0.811, 0.781, 0.456, 0.813, 0.773, 0.460, 0.815, 0.764, + 0.463, 0.816, 0.755, 0.466, 0.817, 0.746, 0.469, 0.818, 0.736, 0.471, + 0.818, 0.725, 0.472, 0.818, 0.715, 0.473, 0.818, 0.703, 0.474, 0.817, + 0.691, 0.474, 0.816, 0.679, 0.474, 0.815, 0.666, 0.474, 0.813, 0.653, + 0.473, 0.811, 0.639, 0.473, 0.809, 0.624, 0.472, 0.806, 0.610, 0.471, + 0.803, 0.594, 0.469, 0.800, 0.579, 0.468, 0.796, 0.562, 0.467, 0.792, + 0.546, 0.466, 0.788, 0.529, 0.464, 0.784, 0.512, 0.463, 0.780, 0.494, + 0.462, 0.775, 0.476, 0.460, 0.770, 0.458, 0.459, 0.765, 0.439, 0.458, + 0.759, 0.420, 0.457, 0.754, 0.401, 0.456, 0.748, 0.381, 0.455, 0.742, + 0.361, 0.454, 0.736, 0.341, 0.453, 0.730, 0.320, 0.453, 0.724, 0.299, + 0.452, 0.718, 0.277, 0.452, 0.711, 0.254, 0.451, 0.705, 0.231, 0.451, + 0.698, 0.206, 0.450, 0.691, 0.179, 0.000, 0.683, 0.955, 0.013, 0.689, + 0.948, 0.092, 0.696, 0.941, 0.137, 0.702, 0.935, 0.171, 0.709, 0.928, + 0.200, 0.715, 0.922, 0.225, 0.721, 0.916, 0.247, 0.727, 0.909, 0.267, + 0.733, 0.904, 0.286, 0.739, 0.898, 0.303, 0.745, 0.892, 0.320, 0.750, + 0.886, 0.335, 0.756, 0.881, 0.350, 0.761, 0.875, 0.363, 0.766, 0.870, + 0.376, 0.771, 0.864, 0.388, 0.776, 0.859, 0.400, 0.781, 0.853, 0.411, + 0.785, 0.847, 0.421, 0.790, 0.842, 0.430, 0.794, 0.836, 0.439, 0.798, + 0.830, 0.448, 0.802, 0.824, 0.455, 0.805, 0.817, 0.462, 0.808, 0.810, + 0.469, 0.811, 0.804, 0.475, 0.814, 0.796, 0.480, 0.816, 0.789, 0.484, + 0.818, 0.781, 0.488, 0.820, 0.772, 0.492, 0.821, 0.763, 0.495, 0.822, + 0.754, 0.497, 0.823, 0.744, 0.499, 0.823, 0.734, 0.500, 0.823, 0.723, + 0.501, 0.823, 0.712, 0.501, 0.822, 0.700, 0.501, 0.821, 0.687, 0.501, + 0.819, 0.674, 0.500, 0.818, 0.661, 0.499, 0.815, 0.647, 0.498, 0.813, + 0.632, 0.497, 0.810, 0.617, 0.496, 0.807, 0.602, 0.494, 0.804, 0.586, + 0.493, 0.800, 0.569, 0.491, 0.796, 0.553, 0.490, 0.792, 0.536, 0.488, + 0.787, 0.518, 0.486, 0.783, 0.500, 0.485, 0.778, 0.482, 0.483, 0.773, + 0.463, 0.482, 0.767, 0.445, 0.480, 0.762, 0.425, 0.479, 0.756, 0.406, + 0.478, 0.750, 0.386, 0.477, 0.744, 0.366, 0.476, 0.738, 0.345, 0.475, + 0.732, 0.325, 0.474, 0.726, 0.303, 0.473, 0.719, 0.281, 0.472, 0.713, + 0.258, 0.471, 0.706, 0.235, 0.470, 0.699, 0.210, 0.469, 0.692, 0.184, + 0.095, 0.683, 0.958, 0.139, 0.690, 0.951, 0.173, 0.697, 0.944, 0.201, + 0.703, 0.938, 0.226, 0.710, 0.931, 0.249, 0.716, 0.925, 0.269, 0.723, + 0.919, 0.288, 0.729, 0.913, 0.306, 0.735, 0.907, 0.323, 0.741, 0.902, + 0.339, 0.747, 0.896, 0.354, 0.752, 0.891, 0.368, 0.758, 0.885, 0.382, + 0.764, 0.880, 0.394, 0.769, 0.875, 0.407, 0.774, 0.869, 0.418, 0.779, + 0.864, 0.429, 0.784, 0.859, 0.440, 0.789, 0.853, 0.450, 0.793, 0.848, + 0.459, 0.798, 0.842, 0.468, 0.802, 0.836, 0.476, 0.806, 0.830, 0.483, + 0.809, 0.824, 0.490, 0.812, 0.818, 0.496, 0.815, 0.811, 0.502, 0.818, + 0.804, 0.507, 0.821, 0.796, 0.512, 0.823, 0.789, 0.515, 0.825, 0.780, + 0.519, 0.826, 0.772, 0.521, 0.827, 0.762, 0.524, 0.828, 0.753, 0.525, + 0.828, 0.742, 0.526, 0.828, 0.732, 0.527, 0.828, 0.720, 0.527, 0.827, + 0.708, 0.527, 0.826, 0.696, 0.526, 0.824, 0.683, 0.525, 0.822, 0.669, + 0.524, 0.820, 0.655, 0.523, 0.817, 0.640, 0.522, 0.814, 0.625, 0.520, + 0.811, 0.609, 0.518, 0.808, 0.593, 0.516, 0.804, 0.576, 0.515, 0.800, + 0.559, 0.513, 0.795, 0.542, 0.511, 0.791, 0.524, 0.509, 0.786, 0.506, + 0.507, 0.781, 0.488, 0.505, 0.775, 0.469, 0.504, 0.770, 0.450, 0.502, + 0.764, 0.431, 0.500, 0.759, 0.411, 0.499, 0.753, 0.391, 0.497, 0.746, + 0.371, 0.496, 0.740, 0.350, 0.495, 0.734, 0.329, 0.494, 0.727, 0.307, + 0.492, 0.721, 0.285, 0.491, 0.714, 0.262, 0.490, 0.707, 0.239, 0.489, + 0.700, 0.214, 0.488, 0.693, 0.188, 0.172, 0.684, 0.961, 0.201, 0.691, + 0.954, 0.226, 0.698, 0.947, 0.248, 0.704, 0.941, 0.269, 0.711, 0.934, + 0.289, 0.717, 0.928, 0.307, 0.724, 0.922, 0.324, 0.730, 0.917, 0.340, + 0.736, 0.911, 0.356, 0.743, 0.906, 0.370, 0.749, 0.900, 0.384, 0.755, + 0.895, 0.398, 0.760, 0.890, 0.411, 0.766, 0.885, 0.423, 0.772, 0.880, + 0.435, 0.777, 0.874, 0.446, 0.782, 0.869, 0.457, 0.787, 0.864, 0.467, + 0.792, 0.859, 0.477, 0.797, 0.854, 0.486, 0.801, 0.848, 0.494, 0.806, + 0.843, 0.502, 0.810, 0.837, 0.510, 0.813, 0.831, 0.517, 0.817, 0.825, + 0.523, 0.820, 0.818, 0.528, 0.823, 0.811, 0.533, 0.825, 0.804, 0.538, + 0.828, 0.797, 0.542, 0.829, 0.788, 0.545, 0.831, 0.780, 0.547, 0.832, + 0.771, 0.549, 0.833, 0.761, 0.551, 0.833, 0.751, 0.552, 0.833, 0.740, + 0.552, 0.833, 0.729, 0.552, 0.832, 0.717, 0.551, 0.830, 0.704, 0.551, + 0.829, 0.691, 0.550, 0.827, 0.677, 0.548, 0.824, 0.663, 0.547, 0.822, + 0.648, 0.545, 0.819, 0.632, 0.543, 0.815, 0.617, 0.541, 0.812, 0.600, + 0.539, 0.808, 0.583, 0.537, 0.803, 0.566, 0.535, 0.799, 0.549, 0.533, + 0.794, 0.531, 0.531, 0.789, 0.512, 0.529, 0.784, 0.494, 0.527, 0.778, + 0.475, 0.525, 0.773, 0.455, 0.523, 0.767, 0.436, 0.521, 0.761, 0.416, + 0.519, 0.755, 0.396, 0.517, 0.748, 0.375, 0.516, 0.742, 0.354, 0.514, + 0.735, 0.333, 0.513, 0.729, 0.311, 0.511, 0.722, 0.289, 0.510, 0.715, + 0.266, 0.509, 0.708, 0.242, 0.507, 0.701, 0.218, 0.506, 0.694, 0.191, + 0.224, 0.684, 0.963, 0.247, 0.691, 0.956, 0.268, 0.698, 0.950, 0.287, + 0.705, 0.943, 0.305, 0.712, 0.937, 0.323, 0.719, 0.931, 0.339, 0.725, + 0.926, 0.355, 0.732, 0.920, 0.370, 0.738, 0.915, 0.385, 0.744, 0.909, + 0.399, 0.751, 0.904, 0.412, 0.757, 0.899, 0.425, 0.763, 0.894, 0.438, + 0.768, 0.889, 0.450, 0.774, 0.884, 0.461, 0.780, 0.879, 0.472, 0.785, + 0.875, 0.483, 0.790, 0.870, 0.493, 0.795, 0.865, 0.502, 0.800, 0.860, + 0.511, 0.805, 0.855, 0.520, 0.809, 0.849, 0.528, 0.814, 0.844, 0.535, + 0.818, 0.838, 0.542, 0.821, 0.832, 0.548, 0.824, 0.826, 0.554, 0.827, + 0.819, 0.559, 0.830, 0.812, 0.563, 0.832, 0.805, 0.567, 0.834, 0.797, + 0.570, 0.836, 0.788, 0.572, 0.837, 0.779, 0.574, 0.838, 0.770, 0.575, + 0.838, 0.760, 0.576, 0.838, 0.749, 0.576, 0.838, 0.737, 0.576, 0.837, + 0.725, 0.575, 0.835, 0.713, 0.574, 0.834, 0.699, 0.573, 0.831, 0.685, + 0.571, 0.829, 0.671, 0.570, 0.826, 0.656, 0.568, 0.823, 0.640, 0.566, + 0.819, 0.624, 0.563, 0.815, 0.607, 0.561, 0.811, 0.590, 0.559, 0.807, + 0.573, 0.556, 0.802, 0.555, 0.554, 0.797, 0.537, 0.552, 0.792, 0.518, + 0.549, 0.786, 0.499, 0.547, 0.781, 0.480, 0.545, 0.775, 0.460, 0.543, + 0.769, 0.441, 0.541, 0.763, 0.420, 0.539, 0.756, 0.400, 0.537, 0.750, + 0.379, 0.535, 0.743, 0.358, 0.533, 0.737, 0.337, 0.531, 0.730, 0.315, + 0.530, 0.723, 0.293, 0.528, 0.716, 0.270, 0.527, 0.709, 0.246, 0.525, + 0.702, 0.221, 0.524, 0.694, 0.195, 0.265, 0.685, 0.965, 0.284, 0.692, + 0.959, 0.303, 0.699, 0.952, 0.320, 0.706, 0.946, 0.337, 0.713, 0.940, + 0.353, 0.720, 0.935, 0.369, 0.726, 0.929, 0.384, 0.733, 0.924, 0.398, + 0.739, 0.918, 0.412, 0.746, 0.913, 0.425, 0.752, 0.908, 0.438, 0.759, + 0.903, 0.451, 0.765, 0.899, 0.463, 0.771, 0.894, 0.475, 0.777, 0.889, + 0.486, 0.782, 0.884, 0.497, 0.788, 0.880, 0.507, 0.793, 0.875, 0.517, + 0.799, 0.870, 0.527, 0.804, 0.866, 0.536, 0.809, 0.861, 0.544, 0.813, + 0.856, 0.552, 0.818, 0.850, 0.560, 0.822, 0.845, 0.566, 0.826, 0.839, + 0.573, 0.829, 0.833, 0.578, 0.832, 0.827, 0.583, 0.835, 0.820, 0.587, + 0.837, 0.813, 0.591, 0.839, 0.805, 0.594, 0.841, 0.797, 0.596, 0.842, + 0.788, 0.598, 0.843, 0.778, 0.599, 0.843, 0.768, 0.600, 0.843, 0.758, + 0.600, 0.843, 0.746, 0.599, 0.842, 0.734, 0.599, 0.840, 0.721, 0.597, + 0.838, 0.708, 0.596, 0.836, 0.694, 0.594, 0.834, 0.679, 0.592, 0.831, + 0.663, 0.590, 0.827, 0.648, 0.587, 0.823, 0.631, 0.585, 0.819, 0.614, + 0.582, 0.815, 0.597, 0.580, 0.810, 0.579, 0.577, 0.805, 0.561, 0.575, + 0.800, 0.542, 0.572, 0.795, 0.524, 0.569, 0.789, 0.504, 0.567, 0.783, + 0.485, 0.565, 0.777, 0.465, 0.562, 0.771, 0.445, 0.560, 0.765, 0.425, + 0.558, 0.758, 0.404, 0.556, 0.752, 0.383, 0.554, 0.745, 0.362, 0.552, + 0.738, 0.341, 0.550, 0.731, 0.319, 0.548, 0.724, 0.296, 0.546, 0.717, + 0.273, 0.544, 0.709, 0.249, 0.542, 0.702, 0.224, 0.541, 0.695, 0.198, + 0.299, 0.685, 0.968, 0.317, 0.692, 0.961, 0.334, 0.699, 0.955, 0.350, + 0.706, 0.949, 0.366, 0.713, 0.943, 0.381, 0.720, 0.938, 0.395, 0.727, + 0.932, 0.410, 0.734, 0.927, 0.423, 0.741, 0.922, 0.437, 0.747, 0.917, + 0.450, 0.754, 0.912, 0.463, 0.760, 0.907, 0.475, 0.767, 0.903, 0.487, + 0.773, 0.898, 0.498, 0.779, 0.894, 0.509, 0.785, 0.889, 0.520, 0.791, + 0.885, 0.531, 0.796, 0.880, 0.540, 0.802, 0.876, 0.550, 0.807, 0.871, + 0.559, 0.812, 0.867, 0.568, 0.817, 0.862, 0.576, 0.822, 0.857, 0.583, + 0.826, 0.852, 0.590, 0.830, 0.847, 0.596, 0.834, 0.841, 0.602, 0.837, + 0.835, 0.607, 0.840, 0.828, 0.611, 0.843, 0.821, 0.615, 0.845, 0.814, + 0.618, 0.846, 0.805, 0.620, 0.848, 0.797, 0.622, 0.848, 0.787, 0.623, + 0.849, 0.777, 0.623, 0.849, 0.766, 0.623, 0.848, 0.755, 0.622, 0.847, + 0.743, 0.621, 0.845, 0.730, 0.620, 0.843, 0.716, 0.618, 0.841, 0.702, + 0.616, 0.838, 0.687, 0.613, 0.835, 0.671, 0.611, 0.831, 0.655, 0.608, + 0.827, 0.638, 0.606, 0.823, 0.621, 0.603, 0.818, 0.604, 0.600, 0.814, + 0.585, 0.597, 0.808, 0.567, 0.594, 0.803, 0.548, 0.592, 0.797, 0.529, + 0.589, 0.792, 0.510, 0.586, 0.785, 0.490, 0.584, 0.779, 0.470, 0.581, + 0.773, 0.450, 0.579, 0.766, 0.429, 0.576, 0.760, 0.408, 0.574, 0.753, + 0.387, 0.572, 0.746, 0.366, 0.569, 0.739, 0.344, 0.567, 0.732, 0.322, + 0.565, 0.725, 0.299, 0.563, 0.717, 0.276, 0.561, 0.710, 0.252, 0.559, + 0.703, 0.227, 0.557, 0.695, 0.201, 0.329, 0.685, 0.970, 0.346, 0.692, + 0.964, 0.362, 0.699, 0.958, 0.377, 0.707, 0.952, 0.392, 0.714, 0.946, + 0.406, 0.721, 0.941, 0.420, 0.728, 0.935, 0.434, 0.735, 0.930, 0.447, + 0.742, 0.925, 0.460, 0.749, 0.920, 0.473, 0.756, 0.916, 0.485, 0.762, + 0.911, 0.497, 0.769, 0.907, 0.509, 0.775, 0.903, 0.521, 0.781, 0.898, + 0.532, 0.788, 0.894, 0.542, 0.794, 0.890, 0.553, 0.799, 0.886, 0.563, + 0.805, 0.882, 0.572, 0.811, 0.877, 0.581, 0.816, 0.873, 0.590, 0.821, + 0.868, 0.598, 0.826, 0.864, 0.606, 0.830, 0.859, 0.613, 0.834, 0.854, + 0.619, 0.838, 0.848, 0.625, 0.842, 0.842, 0.630, 0.845, 0.836, 0.634, + 0.848, 0.829, 0.638, 0.850, 0.822, 0.641, 0.852, 0.814, 0.643, 0.853, + 0.805, 0.645, 0.854, 0.796, 0.645, 0.854, 0.786, 0.646, 0.854, 0.775, + 0.645, 0.853, 0.764, 0.645, 0.852, 0.751, 0.643, 0.851, 0.738, 0.642, + 0.848, 0.725, 0.639, 0.846, 0.710, 0.637, 0.843, 0.695, 0.635, 0.839, + 0.679, 0.632, 0.836, 0.662, 0.629, 0.831, 0.645, 0.626, 0.827, 0.628, + 0.623, 0.822, 0.610, 0.620, 0.817, 0.592, 0.617, 0.811, 0.573, 0.614, + 0.806, 0.554, 0.611, 0.800, 0.534, 0.608, 0.794, 0.515, 0.605, 0.788, + 0.495, 0.602, 0.781, 0.474, 0.599, 0.775, 0.454, 0.597, 0.768, 0.433, + 0.594, 0.761, 0.412, 0.592, 0.754, 0.391, 0.589, 0.747, 0.369, 0.587, + 0.740, 0.347, 0.584, 0.733, 0.325, 0.582, 0.725, 0.302, 0.580, 0.718, + 0.279, 0.577, 0.710, 0.255, 0.575, 0.703, 0.230, 0.573, 0.695, 0.204, + 0.357, 0.685, 0.972, 0.372, 0.692, 0.966, 0.387, 0.700, 0.960, 0.401, + 0.707, 0.954, 0.416, 0.714, 0.949, 0.429, 0.722, 0.943, 0.443, 0.729, + 0.938, 0.456, 0.736, 0.933, 0.469, 0.743, 0.929, 0.482, 0.750, 0.924, + 0.494, 0.757, 0.919, 0.507, 0.764, 0.915, 0.519, 0.771, 0.911, 0.530, + 0.777, 0.907, 0.542, 0.784, 0.903, 0.553, 0.790, 0.899, 0.563, 0.796, + 0.895, 0.574, 0.802, 0.891, 0.584, 0.808, 0.887, 0.593, 0.814, 0.883, + 0.603, 0.820, 0.879, 0.611, 0.825, 0.875, 0.620, 0.830, 0.870, 0.627, + 0.835, 0.866, 0.635, 0.839, 0.861, 0.641, 0.843, 0.856, 0.647, 0.847, + 0.850, 0.652, 0.850, 0.844, 0.657, 0.853, 0.838, 0.660, 0.855, 0.831, + 0.663, 0.857, 0.823, 0.666, 0.859, 0.814, 0.667, 0.859, 0.805, 0.668, + 0.860, 0.795, 0.668, 0.860, 0.784, 0.667, 0.859, 0.773, 0.666, 0.858, + 0.760, 0.665, 0.856, 0.747, 0.663, 0.853, 0.733, 0.661, 0.851, 0.718, + 0.658, 0.847, 0.703, 0.655, 0.844, 0.687, 0.652, 0.840, 0.670, 0.649, + 0.835, 0.652, 0.646, 0.830, 0.635, 0.642, 0.825, 0.616, 0.639, 0.820, + 0.598, 0.636, 0.814, 0.579, 0.633, 0.808, 0.559, 0.629, 0.802, 0.539, + 0.626, 0.796, 0.519, 0.623, 0.790, 0.499, 0.620, 0.783, 0.479, 0.617, + 0.776, 0.458, 0.614, 0.769, 0.437, 0.611, 0.762, 0.416, 0.609, 0.755, + 0.394, 0.606, 0.748, 0.372, 0.603, 0.740, 0.350, 0.601, 0.733, 0.328, + 0.598, 0.726, 0.305, 0.596, 0.718, 0.282, 0.593, 0.710, 0.257, 0.591, + 0.703, 0.232, 0.589, 0.695, 0.206, 0.381, 0.684, 0.974, 0.396, 0.692, + 0.968, 0.410, 0.700, 0.962, 0.424, 0.707, 0.957, 0.438, 0.715, 0.951, + 0.451, 0.722, 0.946, 0.464, 0.729, 0.941, 0.477, 0.737, 0.936, 0.490, + 0.744, 0.932, 0.503, 0.751, 0.927, 0.515, 0.758, 0.923, 0.527, 0.765, + 0.919, 0.539, 0.772, 0.915, 0.550, 0.779, 0.911, 0.562, 0.786, 0.907, + 0.573, 0.792, 0.903, 0.584, 0.799, 0.900, 0.594, 0.805, 0.896, 0.604, + 0.811, 0.892, 0.614, 0.817, 0.889, 0.623, 0.823, 0.885, 0.632, 0.829, + 0.881, 0.641, 0.834, 0.877, 0.649, 0.839, 0.873, 0.656, 0.844, 0.868, + 0.663, 0.848, 0.863, 0.669, 0.852, 0.858, 0.674, 0.855, 0.852, 0.679, + 0.858, 0.846, 0.682, 0.861, 0.839, 0.685, 0.863, 0.832, 0.688, 0.864, + 0.823, 0.689, 0.865, 0.814, 0.690, 0.865, 0.804, 0.690, 0.865, 0.794, + 0.689, 0.864, 0.782, 0.688, 0.863, 0.769, 0.686, 0.861, 0.756, 0.684, + 0.858, 0.742, 0.681, 0.855, 0.726, 0.678, 0.852, 0.711, 0.675, 0.848, + 0.694, 0.672, 0.844, 0.677, 0.668, 0.839, 0.659, 0.665, 0.834, 0.641, + 0.662, 0.829, 0.622, 0.658, 0.823, 0.603, 0.655, 0.817, 0.584, 0.651, + 0.811, 0.564, 0.648, 0.805, 0.544, 0.644, 0.798, 0.524, 0.641, 0.791, + 0.503, 0.638, 0.785, 0.483, 0.635, 0.778, 0.462, 0.631, 0.770, 0.440, + 0.628, 0.763, 0.419, 0.625, 0.756, 0.397, 0.623, 0.748, 0.375, 0.620, + 0.741, 0.353, 0.617, 0.733, 0.330, 0.614, 0.726, 0.307, 0.612, 0.718, + 0.284, 0.609, 0.710, 0.260, 0.606, 0.702, 0.235, 0.604, 0.694, 0.208, + 0.404, 0.684, 0.977, 0.418, 0.692, 0.971, 0.432, 0.699, 0.965, 0.445, + 0.707, 0.959, 0.458, 0.715, 0.954, 0.472, 0.722, 0.949, 0.484, 0.730, + 0.944, 0.497, 0.737, 0.939, 0.510, 0.745, 0.935, 0.522, 0.752, 0.931, + 0.534, 0.759, 0.926, 0.546, 0.767, 0.922, 0.558, 0.774, 0.919, 0.569, + 0.781, 0.915, 0.581, 0.788, 0.911, 0.592, 0.794, 0.908, 0.603, 0.801, + 0.904, 0.613, 0.808, 0.901, 0.624, 0.814, 0.897, 0.633, 0.820, 0.894, + 0.643, 0.826, 0.891, 0.652, 0.832, 0.887, 0.661, 0.838, 0.883, 0.669, + 0.843, 0.879, 0.677, 0.848, 0.875, 0.684, 0.853, 0.871, 0.690, 0.857, + 0.866, 0.695, 0.860, 0.860, 0.700, 0.864, 0.855, 0.704, 0.866, 0.848, + 0.707, 0.869, 0.841, 0.709, 0.870, 0.833, 0.711, 0.871, 0.824, 0.711, + 0.871, 0.814, 0.711, 0.871, 0.803, 0.710, 0.870, 0.791, 0.709, 0.868, + 0.778, 0.707, 0.866, 0.765, 0.704, 0.864, 0.750, 0.701, 0.860, 0.735, + 0.698, 0.857, 0.718, 0.695, 0.852, 0.702, 0.691, 0.848, 0.684, 0.688, + 0.843, 0.666, 0.684, 0.837, 0.647, 0.680, 0.832, 0.628, 0.676, 0.826, + 0.609, 0.673, 0.820, 0.589, 0.669, 0.813, 0.569, 0.665, 0.807, 0.549, + 0.662, 0.800, 0.528, 0.658, 0.793, 0.507, 0.655, 0.786, 0.486, 0.651, + 0.779, 0.465, 0.648, 0.771, 0.444, 0.645, 0.764, 0.422, 0.642, 0.756, + 0.400, 0.639, 0.749, 0.378, 0.636, 0.741, 0.356, 0.633, 0.733, 0.333, + 0.630, 0.726, 0.310, 0.627, 0.718, 0.286, 0.624, 0.710, 0.262, 0.621, + 0.702, 0.237, 0.619, 0.694, 0.210, 0.425, 0.683, 0.979, 0.439, 0.691, + 0.973, 0.452, 0.699, 0.967, 0.465, 0.707, 0.962, 0.478, 0.715, 0.956, + 0.491, 0.722, 0.951, 0.503, 0.730, 0.947, 0.516, 0.738, 0.942, 0.528, + 0.745, 0.938, 0.540, 0.753, 0.934, 0.552, 0.760, 0.930, 0.564, 0.768, + 0.926, 0.576, 0.775, 0.922, 0.588, 0.782, 0.919, 0.599, 0.789, 0.915, + 0.610, 0.797, 0.912, 0.621, 0.803, 0.909, 0.632, 0.810, 0.906, 0.642, + 0.817, 0.902, 0.652, 0.823, 0.899, 0.662, 0.830, 0.896, 0.671, 0.836, + 0.893, 0.680, 0.842, 0.890, 0.689, 0.847, 0.886, 0.697, 0.853, 0.882, + 0.704, 0.857, 0.878, 0.710, 0.862, 0.874, 0.716, 0.866, 0.869, 0.721, + 0.869, 0.863, 0.725, 0.872, 0.857, 0.729, 0.874, 0.850, 0.731, 0.876, + 0.842, 0.732, 0.877, 0.833, 0.733, 0.877, 0.823, 0.732, 0.877, 0.812, + 0.731, 0.876, 0.800, 0.729, 0.874, 0.787, 0.727, 0.872, 0.773, 0.724, + 0.869, 0.759, 0.721, 0.865, 0.743, 0.718, 0.861, 0.726, 0.714, 0.857, + 0.709, 0.710, 0.852, 0.691, 0.706, 0.846, 0.672, 0.702, 0.841, 0.653, + 0.698, 0.835, 0.634, 0.694, 0.828, 0.614, 0.690, 0.822, 0.594, 0.686, + 0.815, 0.574, 0.683, 0.808, 0.553, 0.679, 0.801, 0.532, 0.675, 0.794, + 0.511, 0.672, 0.787, 0.490, 0.668, 0.779, 0.468, 0.665, 0.772, 0.446, + 0.661, 0.764, 0.425, 0.658, 0.757, 0.403, 0.654, 0.749, 0.380, 0.651, + 0.741, 0.358, 0.648, 0.733, 0.335, 0.645, 0.725, 0.312, 0.642, 0.717, + 0.288, 0.639, 0.709, 0.264, 0.636, 0.701, 0.238, 0.633, 0.693, 0.212, + 0.445, 0.682, 0.981, 0.458, 0.691, 0.975, 0.471, 0.699, 0.969, 0.484, + 0.707, 0.964, 0.496, 0.715, 0.959, 0.509, 0.722, 0.954, 0.521, 0.730, + 0.949, 0.534, 0.738, 0.945, 0.546, 0.746, 0.941, 0.558, 0.753, 0.937, + 0.570, 0.761, 0.933, 0.582, 0.769, 0.929, 0.593, 0.776, 0.926, 0.605, + 0.784, 0.922, 0.616, 0.791, 0.919, 0.628, 0.798, 0.916, 0.639, 0.806, + 0.913, 0.649, 0.813, 0.910, 0.660, 0.820, 0.907, 0.670, 0.826, 0.904, + 0.680, 0.833, 0.902, 0.690, 0.839, 0.899, 0.699, 0.846, 0.896, 0.708, + 0.851, 0.893, 0.716, 0.857, 0.889, 0.724, 0.862, 0.885, 0.731, 0.867, + 0.881, 0.737, 0.871, 0.877, 0.742, 0.875, 0.872, 0.746, 0.878, 0.866, + 0.750, 0.880, 0.859, 0.752, 0.882, 0.851, 0.753, 0.883, 0.843, 0.754, + 0.883, 0.833, 0.753, 0.883, 0.822, 0.752, 0.882, 0.810, 0.750, 0.880, + 0.797, 0.747, 0.877, 0.782, 0.744, 0.874, 0.767, 0.740, 0.870, 0.751, + 0.737, 0.866, 0.734, 0.733, 0.861, 0.716, 0.729, 0.855, 0.697, 0.724, + 0.850, 0.678, 0.720, 0.844, 0.659, 0.716, 0.837, 0.639, 0.712, 0.831, + 0.619, 0.708, 0.824, 0.598, 0.704, 0.817, 0.578, 0.699, 0.810, 0.557, + 0.696, 0.803, 0.535, 0.692, 0.795, 0.514, 0.688, 0.788, 0.493, 0.684, + 0.780, 0.471, 0.680, 0.772, 0.449, 0.677, 0.765, 0.427, 0.673, 0.757, + 0.405, 0.670, 0.749, 0.382, 0.666, 0.741, 0.360, 0.663, 0.733, 0.337, + 0.660, 0.725, 0.313, 0.657, 0.716, 0.289, 0.653, 0.708, 0.265, 0.650, + 0.700, 0.240, 0.647, 0.692, 0.213, 0.464, 0.681, 0.982, 0.476, 0.690, + 0.977, 0.489, 0.698, 0.971, 0.501, 0.706, 0.966, 0.514, 0.714, 0.961, + 0.526, 0.722, 0.956, 0.538, 0.730, 0.952, 0.550, 0.738, 0.947, 0.562, + 0.746, 0.943, 0.574, 0.754, 0.939, 0.586, 0.762, 0.936, 0.598, 0.769, + 0.932, 0.610, 0.777, 0.929, 0.621, 0.785, 0.926, 0.633, 0.792, 0.923, + 0.644, 0.800, 0.920, 0.655, 0.807, 0.917, 0.666, 0.815, 0.915, 0.677, + 0.822, 0.912, 0.688, 0.829, 0.909, 0.698, 0.836, 0.907, 0.708, 0.843, + 0.904, 0.717, 0.849, 0.902, 0.727, 0.855, 0.899, 0.735, 0.861, 0.896, + 0.743, 0.867, 0.893, 0.750, 0.872, 0.889, 0.757, 0.877, 0.885, 0.762, + 0.881, 0.880, 0.767, 0.884, 0.875, 0.770, 0.887, 0.868, 0.773, 0.888, + 0.861, 0.774, 0.889, 0.852, 0.774, 0.890, 0.842, 0.774, 0.889, 0.831, + 0.772, 0.888, 0.819, 0.770, 0.885, 0.806, 0.767, 0.883, 0.791, 0.763, + 0.879, 0.775, 0.759, 0.875, 0.759, 0.755, 0.870, 0.741, 0.751, 0.865, + 0.723, 0.747, 0.859, 0.704, 0.742, 0.853, 0.684, 0.738, 0.847, 0.664, + 0.733, 0.840, 0.644, 0.729, 0.833, 0.623, 0.724, 0.826, 0.603, 0.720, + 0.819, 0.581, 0.716, 0.811, 0.560, 0.712, 0.804, 0.539, 0.708, 0.796, + 0.517, 0.704, 0.788, 0.495, 0.700, 0.780, 0.473, 0.696, 0.772, 0.451, + 0.692, 0.764, 0.429, 0.688, 0.756, 0.407, 0.685, 0.748, 0.384, 0.681, + 0.740, 0.361, 0.678, 0.732, 0.338, 0.674, 0.724, 0.315, 0.671, 0.715, + 0.291, 0.667, 0.707, 0.266, 0.664, 0.699, 0.241, 0.661, 0.691, 0.214, + 0.481, 0.680, 0.984, 0.494, 0.689, 0.978, 0.506, 0.697, 0.973, 0.518, + 0.705, 0.968, 0.530, 0.713, 0.963, 0.542, 0.722, 0.958, 0.554, 0.730, + 0.954, 0.566, 0.738, 0.950, 0.578, 0.746, 0.946, 0.590, 0.754, 0.942, + 0.602, 0.762, 0.939, 0.614, 0.770, 0.935, 0.626, 0.778, 0.932, 0.637, + 0.786, 0.929, 0.649, 0.794, 0.926, 0.660, 0.801, 0.924, 0.671, 0.809, + 0.921, 0.683, 0.817, 0.919, 0.694, 0.824, 0.916, 0.704, 0.832, 0.914, + 0.715, 0.839, 0.912, 0.725, 0.846, 0.910, 0.735, 0.853, 0.908, 0.744, + 0.859, 0.905, 0.753, 0.866, 0.903, 0.762, 0.872, 0.900, 0.770, 0.877, + 0.897, 0.776, 0.882, 0.893, 0.782, 0.886, 0.889, 0.787, 0.890, 0.884, + 0.791, 0.893, 0.878, 0.794, 0.895, 0.871, 0.795, 0.896, 0.862, 0.795, + 0.896, 0.852, 0.794, 0.895, 0.841, 0.792, 0.894, 0.829, 0.789, 0.891, + 0.815, 0.786, 0.888, 0.800, 0.782, 0.884, 0.783, 0.778, 0.879, 0.766, + 0.774, 0.874, 0.748, 0.769, 0.868, 0.729, 0.764, 0.862, 0.710, 0.760, + 0.856, 0.690, 0.755, 0.849, 0.669, 0.750, 0.842, 0.649, 0.745, 0.835, + 0.628, 0.741, 0.827, 0.606, 0.736, 0.820, 0.585, 0.732, 0.812, 0.563, + 0.728, 0.804, 0.542, 0.723, 0.796, 0.520, 0.719, 0.788, 0.498, 0.715, + 0.780, 0.475, 0.711, 0.772, 0.453, 0.707, 0.764, 0.431, 0.703, 0.756, + 0.408, 0.699, 0.748, 0.386, 0.696, 0.739, 0.363, 0.692, 0.731, 0.339, + 0.688, 0.723, 0.316, 0.685, 0.714, 0.292, 0.681, 0.706, 0.267, 0.678, + 0.697, 0.242, 0.674, 0.689, 0.215, 0.498, 0.679, 0.986, 0.510, 0.687, + 0.980, 0.522, 0.696, 0.975, 0.534, 0.704, 0.970, 0.546, 0.712, 0.965, + 0.558, 0.721, 0.961, 0.570, 0.729, 0.956, 0.581, 0.737, 0.952, 0.593, + 0.746, 0.948, 0.605, 0.754, 0.945, 0.617, 0.762, 0.941, 0.629, 0.770, + 0.938, 0.640, 0.778, 0.935, 0.652, 0.786, 0.932, 0.664, 0.794, 0.930, + 0.675, 0.802, 0.927, 0.687, 0.810, 0.925, 0.698, 0.818, 0.923, 0.709, + 0.826, 0.921, 0.720, 0.834, 0.919, 0.731, 0.841, 0.917, 0.742, 0.849, + 0.915, 0.752, 0.856, 0.913, 0.762, 0.863, 0.911, 0.771, 0.870, 0.909, + 0.780, 0.876, 0.907, 0.788, 0.882, 0.904, 0.796, 0.887, 0.901, 0.802, + 0.892, 0.897, 0.807, 0.896, 0.893, 0.811, 0.899, 0.887, 0.814, 0.902, + 0.880, 0.815, 0.903, 0.872, 0.815, 0.903, 0.862, 0.814, 0.902, 0.851, + 0.812, 0.900, 0.838, 0.809, 0.897, 0.824, 0.805, 0.893, 0.808, 0.801, + 0.889, 0.791, 0.796, 0.884, 0.774, 0.791, 0.878, 0.755, 0.786, 0.872, + 0.735, 0.781, 0.865, 0.715, 0.776, 0.858, 0.695, 0.771, 0.851, 0.674, + 0.767, 0.844, 0.653, 0.762, 0.836, 0.631, 0.757, 0.829, 0.610, 0.752, + 0.821, 0.588, 0.748, 0.813, 0.566, 0.743, 0.805, 0.544, 0.739, 0.796, + 0.522, 0.734, 0.788, 0.500, 0.730, 0.780, 0.477, 0.726, 0.772, 0.455, + 0.722, 0.763, 0.432, 0.718, 0.755, 0.410, 0.714, 0.746, 0.387, 0.710, + 0.738, 0.364, 0.706, 0.730, 0.340, 0.702, 0.721, 0.317, 0.698, 0.713, + 0.293, 0.694, 0.704, 0.268, 0.691, 0.696, 0.243, 0.687, 0.687, 0.216, + 0.513, 0.677, 0.987, 0.525, 0.686, 0.982, 0.537, 0.694, 0.977, 0.549, + 0.703, 0.972, 0.561, 0.711, 0.967, 0.572, 0.720, 0.962, 0.584, 0.728, + 0.958, 0.596, 0.737, 0.954, 0.608, 0.745, 0.951, 0.619, 0.753, 0.947, + 0.631, 0.762, 0.944, 0.643, 0.770, 0.941, 0.655, 0.778, 0.938, 0.666, + 0.787, 0.935, 0.678, 0.795, 0.933, 0.689, 0.803, 0.930, 0.701, 0.811, + 0.928, 0.713, 0.820, 0.926, 0.724, 0.828, 0.925, 0.735, 0.836, 0.923, + 0.746, 0.844, 0.921, 0.757, 0.852, 0.920, 0.768, 0.859, 0.918, 0.778, + 0.867, 0.917, 0.788, 0.874, 0.915, 0.797, 0.881, 0.913, 0.806, 0.887, + 0.911, 0.814, 0.893, 0.909, 0.821, 0.898, 0.906, 0.827, 0.902, 0.902, + 0.831, 0.906, 0.897, 0.834, 0.908, 0.890, 0.836, 0.910, 0.882, 0.836, + 0.910, 0.873, 0.834, 0.909, 0.861, 0.832, 0.906, 0.848, 0.828, 0.903, + 0.833, 0.824, 0.899, 0.817, 0.819, 0.894, 0.799, 0.814, 0.888, 0.781, + 0.809, 0.882, 0.761, 0.804, 0.875, 0.741, 0.798, 0.868, 0.720, 0.793, + 0.861, 0.699, 0.788, 0.853, 0.678, 0.783, 0.845, 0.656, 0.777, 0.837, + 0.635, 0.772, 0.829, 0.613, 0.768, 0.821, 0.590, 0.763, 0.813, 0.568, + 0.758, 0.804, 0.546, 0.753, 0.796, 0.524, 0.749, 0.788, 0.501, 0.744, + 0.779, 0.479, 0.740, 0.771, 0.456, 0.736, 0.762, 0.433, 0.731, 0.754, + 0.411, 0.727, 0.745, 0.388, 0.723, 0.736, 0.365, 0.719, 0.728, 0.341, + 0.715, 0.719, 0.317, 0.711, 0.711, 0.293, 0.707, 0.702, 0.268, 0.704, + 0.694, 0.243, 0.700, 0.685, 0.216, 0.528, 0.675, 0.989, 0.540, 0.684, + 0.983, 0.551, 0.693, 0.978, 0.563, 0.701, 0.973, 0.575, 0.710, 0.969, + 0.586, 0.718, 0.964, 0.598, 0.727, 0.960, 0.610, 0.736, 0.956, 0.621, + 0.744, 0.953, 0.633, 0.753, 0.949, 0.645, 0.761, 0.946, 0.656, 0.770, + 0.943, 0.668, 0.778, 0.940, 0.680, 0.787, 0.938, 0.691, 0.795, 0.936, + 0.703, 0.804, 0.933, 0.715, 0.812, 0.932, 0.726, 0.821, 0.930, 0.738, + 0.829, 0.928, 0.749, 0.837, 0.927, 0.761, 0.846, 0.926, 0.772, 0.854, + 0.924, 0.783, 0.862, 0.923, 0.794, 0.870, 0.922, 0.804, 0.877, 0.921, + 0.814, 0.885, 0.920, 0.824, 0.892, 0.918, 0.832, 0.898, 0.917, 0.840, + 0.904, 0.914, 0.846, 0.909, 0.911, 0.851, 0.913, 0.906, 0.855, 0.915, + 0.901, 0.856, 0.917, 0.893, 0.856, 0.917, 0.883, 0.854, 0.915, 0.871, + 0.851, 0.913, 0.858, 0.847, 0.909, 0.842, 0.842, 0.904, 0.825, 0.837, + 0.898, 0.806, 0.831, 0.892, 0.787, 0.826, 0.885, 0.767, 0.820, 0.878, + 0.746, 0.814, 0.870, 0.725, 0.809, 0.862, 0.703, 0.803, 0.854, 0.681, + 0.798, 0.846, 0.659, 0.793, 0.838, 0.637, 0.788, 0.829, 0.615, 0.782, + 0.821, 0.592, 0.777, 0.812, 0.570, 0.773, 0.804, 0.548, 0.768, 0.795, + 0.525, 0.763, 0.787, 0.502, 0.758, 0.778, 0.480, 0.754, 0.769, 0.457, + 0.749, 0.761, 0.434, 0.745, 0.752, 0.411, 0.741, 0.743, 0.388, 0.737, + 0.735, 0.365, 0.732, 0.726, 0.342, 0.728, 0.717, 0.318, 0.724, 0.709, + 0.293, 0.720, 0.700, 0.269, 0.716, 0.691, 0.243, 0.712, 0.683, 0.216, + 0.542, 0.673, 0.990, 0.554, 0.682, 0.985, 0.565, 0.691, 0.980, 0.577, + 0.700, 0.975, 0.588, 0.708, 0.970, 0.600, 0.717, 0.966, 0.611, 0.726, + 0.962, 0.623, 0.734, 0.958, 0.634, 0.743, 0.955, 0.646, 0.752, 0.951, + 0.657, 0.760, 0.948, 0.669, 0.769, 0.945, 0.681, 0.778, 0.943, 0.692, + 0.786, 0.940, 0.704, 0.795, 0.938, 0.716, 0.804, 0.936, 0.728, 0.812, + 0.934, 0.739, 0.821, 0.933, 0.751, 0.830, 0.932, 0.763, 0.838, 0.930, + 0.774, 0.847, 0.929, 0.786, 0.856, 0.929, 0.797, 0.864, 0.928, 0.809, + 0.873, 0.927, 0.819, 0.881, 0.927, 0.830, 0.889, 0.926, 0.840, 0.896, + 0.925, 0.850, 0.903, 0.924, 0.858, 0.910, 0.922, 0.865, 0.915, 0.920, + 0.871, 0.920, 0.916, 0.875, 0.923, 0.911, 0.876, 0.924, 0.903, 0.876, + 0.924, 0.894, 0.873, 0.922, 0.882, 0.870, 0.919, 0.867, 0.865, 0.914, + 0.851, 0.860, 0.909, 0.832, 0.854, 0.902, 0.813, 0.848, 0.895, 0.793, + 0.842, 0.888, 0.772, 0.836, 0.880, 0.750, 0.830, 0.872, 0.729, 0.824, + 0.864, 0.707, 0.819, 0.855, 0.684, 0.813, 0.847, 0.662, 0.808, 0.838, + 0.639, 0.802, 0.829, 0.617, 0.797, 0.820, 0.594, 0.792, 0.812, 0.571, + 0.787, 0.803, 0.549, 0.782, 0.794, 0.526, 0.777, 0.785, 0.503, 0.772, + 0.776, 0.480, 0.767, 0.768, 0.458, 0.763, 0.759, 0.435, 0.758, 0.750, + 0.412, 0.754, 0.741, 0.388, 0.749, 0.732, 0.365, 0.745, 0.724, 0.342, + 0.741, 0.715, 0.318, 0.737, 0.706, 0.293, 0.732, 0.697, 0.269, 0.728, + 0.689, 0.243, 0.724, 0.680, 0.216, 0.556, 0.671, 0.992, 0.567, 0.680, + 0.986, 0.578, 0.689, 0.981, 0.590, 0.697, 0.976, 0.601, 0.706, 0.972, + 0.612, 0.715, 0.968, 0.624, 0.724, 0.964, 0.635, 0.733, 0.960, 0.646, + 0.741, 0.956, 0.658, 0.750, 0.953, 0.670, 0.759, 0.950, 0.681, 0.768, + 0.947, 0.693, 0.777, 0.945, 0.704, 0.786, 0.943, 0.716, 0.794, 0.941, + 0.728, 0.803, 0.939, 0.740, 0.812, 0.937, 0.752, 0.821, 0.936, 0.763, + 0.830, 0.935, 0.775, 0.839, 0.934, 0.787, 0.848, 0.933, 0.799, 0.857, + 0.932, 0.811, 0.866, 0.932, 0.822, 0.875, 0.932, 0.834, 0.883, 0.932, + 0.845, 0.892, 0.931, 0.856, 0.900, 0.931, 0.866, 0.908, 0.931, 0.876, + 0.915, 0.930, 0.884, 0.922, 0.929, 0.890, 0.927, 0.926, 0.895, 0.930, + 0.921, 0.896, 0.932, 0.914, 0.896, 0.932, 0.905, 0.893, 0.929, 0.892, + 0.888, 0.925, 0.876, 0.883, 0.920, 0.859, 0.877, 0.913, 0.840, 0.871, + 0.906, 0.819, 0.864, 0.898, 0.798, 0.858, 0.890, 0.776, 0.852, 0.882, + 0.754, 0.845, 0.873, 0.732, 0.839, 0.864, 0.709, 0.833, 0.855, 0.686, + 0.828, 0.846, 0.664, 0.822, 0.837, 0.641, 0.816, 0.828, 0.618, 0.811, + 0.819, 0.595, 0.806, 0.810, 0.572, 0.800, 0.801, 0.549, 0.795, 0.792, + 0.526, 0.790, 0.783, 0.504, 0.785, 0.774, 0.481, 0.781, 0.765, 0.458, + 0.776, 0.756, 0.435, 0.771, 0.748, 0.412, 0.766, 0.739, 0.388, 0.762, + 0.730, 0.365, 0.757, 0.721, 0.341, 0.753, 0.712, 0.318, 0.749, 0.703, + 0.293, 0.744, 0.695, 0.268, 0.740, 0.686, 0.243, 0.736, 0.677, 0.216, + 0.569, 0.668, 0.993, 0.580, 0.677, 0.987, 0.591, 0.686, 0.982, 0.602, + 0.695, 0.978, 0.613, 0.704, 0.973, 0.624, 0.713, 0.969, 0.635, 0.722, + 0.965, 0.647, 0.731, 0.961, 0.658, 0.740, 0.958, 0.670, 0.748, 0.955, + 0.681, 0.757, 0.952, 0.693, 0.766, 0.949, 0.704, 0.775, 0.947, 0.716, + 0.784, 0.945, 0.728, 0.793, 0.943, 0.739, 0.802, 0.941, 0.751, 0.812, + 0.939, 0.763, 0.821, 0.938, 0.775, 0.830, 0.937, 0.787, 0.839, 0.936, + 0.799, 0.848, 0.936, 0.811, 0.858, 0.936, 0.823, 0.867, 0.935, 0.835, + 0.876, 0.936, 0.847, 0.886, 0.936, 0.859, 0.895, 0.936, 0.870, 0.904, + 0.937, 0.882, 0.912, 0.937, 0.892, 0.920, 0.937, 0.901, 0.928, 0.937, + 0.909, 0.934, 0.936, 0.914, 0.938, 0.932, 0.917, 0.940, 0.926, 0.915, + 0.940, 0.916, 0.912, 0.936, 0.902, 0.906, 0.931, 0.885, 0.900, 0.925, + 0.866, 0.893, 0.917, 0.846, 0.887, 0.909, 0.824, 0.880, 0.900, 0.802, + 0.873, 0.891, 0.780, 0.867, 0.882, 0.757, 0.860, 0.873, 0.734, 0.854, + 0.864, 0.711, 0.848, 0.855, 0.688, 0.842, 0.845, 0.665, 0.836, 0.836, + 0.642, 0.830, 0.827, 0.619, 0.824, 0.818, 0.596, 0.819, 0.808, 0.573, + 0.814, 0.799, 0.549, 0.808, 0.790, 0.527, 0.803, 0.781, 0.504, 0.798, + 0.772, 0.481, 0.793, 0.763, 0.458, 0.788, 0.754, 0.434, 0.783, 0.745, + 0.411, 0.779, 0.736, 0.388, 0.774, 0.727, 0.365, 0.769, 0.718, 0.341, + 0.765, 0.709, 0.317, 0.760, 0.700, 0.293, 0.756, 0.691, 0.268, 0.751, + 0.683, 0.242, 0.747, 0.674, 0.215, 0.581, 0.665, 0.994, 0.592, 0.674, + 0.989, 0.603, 0.683, 0.984, 0.614, 0.692, 0.979, 0.625, 0.701, 0.974, + 0.636, 0.710, 0.970, 0.647, 0.719, 0.966, 0.658, 0.728, 0.963, 0.669, + 0.737, 0.959, 0.681, 0.746, 0.956, 0.692, 0.755, 0.953, 0.703, 0.765, + 0.951, 0.715, 0.774, 0.948, 0.727, 0.783, 0.946, 0.738, 0.792, 0.944, + 0.750, 0.801, 0.943, 0.762, 0.810, 0.941, 0.774, 0.820, 0.940, 0.786, + 0.829, 0.939, 0.798, 0.839, 0.939, 0.810, 0.848, 0.938, 0.822, 0.858, + 0.938, 0.834, 0.867, 0.939, 0.847, 0.877, 0.939, 0.859, 0.887, 0.940, + 0.871, 0.897, 0.940, 0.883, 0.906, 0.942, 0.896, 0.916, 0.943, 0.907, + 0.925, 0.944, 0.918, 0.933, 0.945, 0.927, 0.941, 0.945, 0.934, 0.946, + 0.943, 0.937, 0.949, 0.937, 0.935, 0.948, 0.927, 0.930, 0.943, 0.912, + 0.924, 0.937, 0.893, 0.916, 0.929, 0.872, 0.909, 0.920, 0.850, 0.902, + 0.911, 0.828, 0.895, 0.901, 0.805, 0.888, 0.892, 0.782, 0.881, 0.882, + 0.759, 0.874, 0.872, 0.735, 0.868, 0.863, 0.712, 0.861, 0.853, 0.689, + 0.855, 0.844, 0.665, 0.849, 0.834, 0.642, 0.843, 0.825, 0.619, 0.838, + 0.815, 0.596, 0.832, 0.806, 0.572, 0.826, 0.797, 0.549, 0.821, 0.787, + 0.526, 0.816, 0.778, 0.503, 0.811, 0.769, 0.480, 0.805, 0.760, 0.457, + 0.800, 0.751, 0.434, 0.795, 0.742, 0.411, 0.791, 0.733, 0.387, 0.786, + 0.724, 0.364, 0.781, 0.715, 0.340, 0.776, 0.706, 0.316, 0.772, 0.697, + 0.292, 0.767, 0.688, 0.267, 0.762, 0.679, 0.241, 0.758, 0.670, 0.215, + 0.593, 0.662, 0.995, 0.603, 0.671, 0.990, 0.614, 0.680, 0.985, 0.625, + 0.689, 0.980, 0.636, 0.699, 0.975, 0.647, 0.708, 0.971, 0.658, 0.717, + 0.967, 0.669, 0.726, 0.964, 0.680, 0.735, 0.960, 0.691, 0.744, 0.957, + 0.702, 0.753, 0.955, 0.714, 0.762, 0.952, 0.725, 0.771, 0.950, 0.737, + 0.781, 0.948, 0.748, 0.790, 0.946, 0.760, 0.799, 0.944, 0.772, 0.809, + 0.943, 0.784, 0.818, 0.942, 0.796, 0.828, 0.941, 0.808, 0.837, 0.941, + 0.820, 0.847, 0.940, 0.832, 0.857, 0.941, 0.844, 0.867, 0.941, 0.857, + 0.877, 0.942, 0.869, 0.887, 0.943, 0.882, 0.897, 0.944, 0.895, 0.908, + 0.945, 0.908, 0.918, 0.947, 0.920, 0.928, 0.949, 0.933, 0.938, 0.951, + 0.944, 0.947, 0.953, 0.953, 0.955, 0.954, 0.957, 0.958, 0.950, 0.954, + 0.956, 0.938, 0.948, 0.949, 0.920, 0.940, 0.941, 0.899, 0.932, 0.931, + 0.877, 0.924, 0.921, 0.854, 0.916, 0.911, 0.830, 0.909, 0.901, 0.807, + 0.901, 0.891, 0.783, 0.894, 0.881, 0.759, 0.887, 0.871, 0.736, 0.881, + 0.861, 0.712, 0.874, 0.851, 0.688, 0.868, 0.841, 0.665, 0.862, 0.832, + 0.642, 0.856, 0.822, 0.618, 0.850, 0.812, 0.595, 0.844, 0.803, 0.572, + 0.839, 0.793, 0.549, 0.833, 0.784, 0.525, 0.828, 0.775, 0.502, 0.822, + 0.766, 0.479, 0.817, 0.756, 0.456, 0.812, 0.747, 0.433, 0.807, 0.738, + 0.410, 0.802, 0.729, 0.387, 0.797, 0.720, 0.363, 0.792, 0.711, 0.339, + 0.787, 0.702, 0.315, 0.782, 0.693, 0.291, 0.778, 0.684, 0.266, 0.773, + 0.675, 0.240, 0.768, 0.666, 0.214, 0.604, 0.659, 0.996, 0.614, 0.668, + 0.990, 0.625, 0.677, 0.985, 0.636, 0.686, 0.981, 0.646, 0.695, 0.976, + 0.657, 0.704, 0.972, 0.668, 0.714, 0.968, 0.679, 0.723, 0.965, 0.690, + 0.732, 0.961, 0.701, 0.741, 0.958, 0.712, 0.750, 0.956, 0.723, 0.759, + 0.953, 0.735, 0.769, 0.951, 0.746, 0.778, 0.949, 0.758, 0.787, 0.947, + 0.769, 0.797, 0.945, 0.781, 0.806, 0.944, 0.793, 0.816, 0.943, 0.805, + 0.826, 0.943, 0.817, 0.836, 0.942, 0.829, 0.845, 0.942, 0.841, 0.855, + 0.942, 0.853, 0.866, 0.943, 0.866, 0.876, 0.943, 0.879, 0.886, 0.945, + 0.892, 0.897, 0.946, 0.905, 0.907, 0.948, 0.918, 0.918, 0.950, 0.931, + 0.930, 0.953, 0.945, 0.941, 0.956, 0.958, 0.952, 0.960, 0.971, 0.963, + 0.963, 0.978, 0.968, 0.963, 0.972, 0.963, 0.948, 0.963, 0.954, 0.926, + 0.954, 0.943, 0.903, 0.945, 0.931, 0.879, 0.937, 0.921, 0.855, 0.929, + 0.910, 0.831, 0.921, 0.899, 0.807, 0.914, 0.889, 0.783, 0.907, 0.878, + 0.759, 0.900, 0.868, 0.735, 0.893, 0.858, 0.711, 0.887, 0.848, 0.688, + 0.880, 0.838, 0.664, 0.874, 0.828, 0.641, 0.868, 0.818, 0.617, 0.862, + 0.809, 0.594, 0.856, 0.799, 0.571, 0.850, 0.790, 0.547, 0.845, 0.780, + 0.524, 0.839, 0.771, 0.501, 0.834, 0.762, 0.478, 0.828, 0.752, 0.455, + 0.823, 0.743, 0.432, 0.818, 0.734, 0.409, 0.813, 0.725, 0.385, 0.808, + 0.716, 0.362, 0.803, 0.707, 0.338, 0.798, 0.698, 0.314, 0.793, 0.689, + 0.290, 0.788, 0.680, 0.265, 0.783, 0.671, 0.239, 0.778, 0.662, 0.213, + 0.615, 0.655, 0.996, 0.625, 0.664, 0.991, 0.635, 0.673, 0.986, 0.646, + 0.683, 0.982, 0.656, 0.692, 0.977, 0.667, 0.701, 0.973, 0.678, 0.710, + 0.969, 0.688, 0.719, 0.966, 0.699, 0.729, 0.962, 0.710, 0.738, 0.959, + 0.721, 0.747, 0.956, 0.732, 0.756, 0.954, 0.744, 0.766, 0.952, 0.755, + 0.775, 0.950, 0.766, 0.784, 0.948, 0.778, 0.794, 0.946, 0.789, 0.804, + 0.945, 0.801, 0.813, 0.944, 0.813, 0.823, 0.943, 0.825, 0.833, 0.943, + 0.837, 0.843, 0.943, 0.849, 0.853, 0.943, 0.861, 0.863, 0.944, 0.874, + 0.873, 0.944, 0.886, 0.884, 0.946, 0.899, 0.895, 0.947, 0.912, 0.906, + 0.949, 0.926, 0.917, 0.952, 0.939, 0.928, 0.955, 0.953, 0.940, 0.958, + 0.967, 0.953, 0.963, 0.982, 0.966, 0.969, 0.994, 0.976, 0.972, 0.986, + 0.966, 0.952, 0.976, 0.953, 0.928, 0.966, 0.941, 0.903, 0.957, 0.929, + 0.878, 0.949, 0.918, 0.854, 0.941, 0.906, 0.830, 0.933, 0.896, 0.806, + 0.926, 0.885, 0.782, 0.918, 0.874, 0.758, 0.911, 0.864, 0.734, 0.905, + 0.854, 0.710, 0.898, 0.844, 0.686, 0.892, 0.834, 0.663, 0.885, 0.824, + 0.639, 0.879, 0.814, 0.616, 0.873, 0.804, 0.592, 0.867, 0.795, 0.569, + 0.861, 0.785, 0.546, 0.856, 0.776, 0.523, 0.850, 0.766, 0.500, 0.845, + 0.757, 0.477, 0.839, 0.748, 0.454, 0.834, 0.739, 0.430, 0.829, 0.729, + 0.407, 0.823, 0.720, 0.384, 0.818, 0.711, 0.361, 0.813, 0.702, 0.337, + 0.808, 0.693, 0.313, 0.803, 0.684, 0.289, 0.798, 0.675, 0.264, 0.793, + 0.666, 0.238, 0.788, 0.657, 0.211, 0.625, 0.651, 0.997, 0.635, 0.660, + 0.992, 0.645, 0.669, 0.987, 0.656, 0.679, 0.982, 0.666, 0.688, 0.978, + 0.676, 0.697, 0.974, 0.687, 0.706, 0.970, 0.698, 0.716, 0.966, 0.708, + 0.725, 0.963, 0.719, 0.734, 0.960, 0.730, 0.743, 0.957, 0.741, 0.753, + 0.954, 0.752, 0.762, 0.952, 0.763, 0.771, 0.950, 0.774, 0.781, 0.948, + 0.786, 0.790, 0.947, 0.797, 0.800, 0.945, 0.809, 0.810, 0.945, 0.820, + 0.819, 0.944, 0.832, 0.829, 0.943, 0.844, 0.839, 0.943, 0.856, 0.849, + 0.943, 0.868, 0.860, 0.944, 0.880, 0.870, 0.945, 0.893, 0.880, 0.946, + 0.905, 0.891, 0.947, 0.918, 0.902, 0.949, 0.931, 0.913, 0.951, 0.944, + 0.924, 0.954, 0.957, 0.936, 0.957, 0.971, 0.947, 0.961, 0.983, 0.958, + 0.964, 0.991, 0.963, 0.962, 0.990, 0.957, 0.946, 0.983, 0.946, 0.924, + 0.975, 0.935, 0.900, 0.967, 0.923, 0.876, 0.959, 0.912, 0.851, 0.951, + 0.901, 0.827, 0.943, 0.890, 0.803, 0.936, 0.880, 0.779, 0.929, 0.869, + 0.755, 0.922, 0.859, 0.732, 0.915, 0.849, 0.708, 0.909, 0.839, 0.684, + 0.902, 0.829, 0.661, 0.896, 0.819, 0.637, 0.890, 0.809, 0.614, 0.884, + 0.800, 0.591, 0.878, 0.790, 0.567, 0.872, 0.780, 0.544, 0.866, 0.771, + 0.521, 0.861, 0.762, 0.498, 0.855, 0.752, 0.475, 0.850, 0.743, 0.452, + 0.844, 0.734, 0.429, 0.839, 0.725, 0.406, 0.834, 0.715, 0.382, 0.828, + 0.706, 0.359, 0.823, 0.697, 0.335, 0.818, 0.688, 0.311, 0.813, 0.679, + 0.287, 0.808, 0.670, 0.262, 0.803, 0.662, 0.236, 0.798, 0.653, 0.210, + 0.635, 0.646, 0.998, 0.645, 0.656, 0.992, 0.655, 0.665, 0.987, 0.665, + 0.674, 0.983, 0.675, 0.684, 0.978, 0.685, 0.693, 0.974, 0.696, 0.702, + 0.970, 0.706, 0.711, 0.966, 0.717, 0.721, 0.963, 0.727, 0.730, 0.960, + 0.738, 0.739, 0.957, 0.749, 0.748, 0.955, 0.760, 0.758, 0.952, 0.771, + 0.767, 0.950, 0.782, 0.777, 0.948, 0.793, 0.786, 0.947, 0.804, 0.796, + 0.946, 0.816, 0.805, 0.945, 0.827, 0.815, 0.944, 0.839, 0.825, 0.943, + 0.850, 0.835, 0.943, 0.862, 0.845, 0.943, 0.874, 0.855, 0.943, 0.886, + 0.865, 0.944, 0.898, 0.875, 0.945, 0.910, 0.886, 0.946, 0.923, 0.896, + 0.948, 0.935, 0.907, 0.949, 0.947, 0.917, 0.951, 0.959, 0.927, 0.953, + 0.970, 0.937, 0.955, 0.980, 0.944, 0.955, 0.987, 0.946, 0.949, 0.989, + 0.942, 0.936, 0.985, 0.935, 0.916, 0.980, 0.925, 0.894, 0.973, 0.915, + 0.871, 0.966, 0.904, 0.847, 0.959, 0.894, 0.824, 0.952, 0.883, 0.800, + 0.945, 0.873, 0.776, 0.938, 0.863, 0.752, 0.932, 0.853, 0.729, 0.925, + 0.843, 0.705, 0.919, 0.833, 0.682, 0.912, 0.823, 0.658, 0.906, 0.813, + 0.635, 0.900, 0.803, 0.611, 0.894, 0.794, 0.588, 0.888, 0.784, 0.565, + 0.882, 0.775, 0.542, 0.876, 0.765, 0.519, 0.871, 0.756, 0.496, 0.865, + 0.747, 0.473, 0.859, 0.738, 0.450, 0.854, 0.728, 0.427, 0.848, 0.719, + 0.404, 0.843, 0.710, 0.380, 0.838, 0.701, 0.357, 0.833, 0.692, 0.333, + 0.827, 0.683, 0.310, 0.822, 0.674, 0.285, 0.817, 0.665, 0.260, 0.812, + 0.656, 0.235, 0.807, 0.648, 0.208, 0.644, 0.642, 0.998, 0.654, 0.651, + 0.993, 0.664, 0.660, 0.988, 0.674, 0.670, 0.983, 0.684, 0.679, 0.978, + 0.694, 0.688, 0.974, 0.704, 0.697, 0.970, 0.715, 0.707, 0.967, 0.725, + 0.716, 0.963, 0.735, 0.725, 0.960, 0.746, 0.735, 0.957, 0.757, 0.744, + 0.955, 0.767, 0.753, 0.952, 0.778, 0.763, 0.950, 0.789, 0.772, 0.948, + 0.800, 0.781, 0.947, 0.811, 0.791, 0.945, 0.822, 0.801, 0.944, 0.833, + 0.810, 0.943, 0.845, 0.820, 0.943, 0.856, 0.830, 0.942, 0.868, 0.839, + 0.942, 0.879, 0.849, 0.942, 0.891, 0.859, 0.943, 0.902, 0.869, 0.943, + 0.914, 0.879, 0.944, 0.926, 0.889, 0.945, 0.937, 0.899, 0.946, 0.949, + 0.908, 0.947, 0.959, 0.917, 0.948, 0.969, 0.924, 0.947, 0.978, 0.929, + 0.944, 0.984, 0.930, 0.937, 0.986, 0.927, 0.924, 0.986, 0.921, 0.907, + 0.982, 0.913, 0.887, 0.978, 0.904, 0.865, 0.972, 0.895, 0.842, 0.966, + 0.885, 0.819, 0.959, 0.875, 0.795, 0.953, 0.865, 0.772, 0.946, 0.855, + 0.749, 0.940, 0.846, 0.725, 0.934, 0.836, 0.702, 0.927, 0.826, 0.678, + 0.921, 0.816, 0.655, 0.915, 0.807, 0.632, 0.909, 0.797, 0.609, 0.903, + 0.788, 0.586, 0.897, 0.778, 0.562, 0.891, 0.769, 0.539, 0.886, 0.759, + 0.516, 0.880, 0.750, 0.493, 0.874, 0.741, 0.471, 0.869, 0.732, 0.448, + 0.863, 0.723, 0.425, 0.858, 0.713, 0.402, 0.852, 0.704, 0.378, 0.847, + 0.695, 0.355, 0.842, 0.686, 0.331, 0.836, 0.677, 0.308, 0.831, 0.669, + 0.283, 0.826, 0.660, 0.258, 0.821, 0.651, 0.233, 0.815, 0.642, 0.206, + 0.653, 0.636, 0.998, 0.663, 0.646, 0.993, 0.673, 0.655, 0.988, 0.682, + 0.665, 0.983, 0.692, 0.674, 0.979, 0.702, 0.683, 0.974, 0.712, 0.692, + 0.970, 0.722, 0.702, 0.967, 0.733, 0.711, 0.963, 0.743, 0.720, 0.960, + 0.753, 0.730, 0.957, 0.764, 0.739, 0.954, 0.774, 0.748, 0.952, 0.785, + 0.757, 0.950, 0.796, 0.767, 0.948, 0.806, 0.776, 0.946, 0.817, 0.786, + 0.945, 0.828, 0.795, 0.943, 0.839, 0.804, 0.942, 0.850, 0.814, 0.941, + 0.861, 0.824, 0.941, 0.872, 0.833, 0.940, 0.884, 0.843, 0.940, 0.895, + 0.852, 0.940, 0.906, 0.862, 0.940, 0.917, 0.871, 0.941, 0.928, 0.880, + 0.941, 0.939, 0.889, 0.941, 0.949, 0.897, 0.941, 0.959, 0.904, 0.940, + 0.968, 0.910, 0.938, 0.975, 0.914, 0.933, 0.981, 0.914, 0.925, 0.984, + 0.912, 0.913, 0.985, 0.907, 0.897, 0.983, 0.900, 0.878, 0.980, 0.893, + 0.857, 0.976, 0.884, 0.836, 0.971, 0.875, 0.813, 0.965, 0.866, 0.790, + 0.959, 0.856, 0.767, 0.954, 0.847, 0.744, 0.947, 0.837, 0.721, 0.941, + 0.828, 0.698, 0.935, 0.818, 0.675, 0.929, 0.809, 0.652, 0.923, 0.800, + 0.629, 0.917, 0.790, 0.605, 0.912, 0.781, 0.582, 0.906, 0.771, 0.560, + 0.900, 0.762, 0.537, 0.894, 0.753, 0.514, 0.889, 0.744, 0.491, 0.883, + 0.735, 0.468, 0.877, 0.725, 0.445, 0.872, 0.716, 0.422, 0.866, 0.707, + 0.399, 0.861, 0.698, 0.376, 0.856, 0.689, 0.353, 0.850, 0.680, 0.329, + 0.845, 0.672, 0.305, 0.840, 0.663, 0.281, 0.834, 0.654, 0.256, 0.829, + 0.645, 0.231, 0.824, 0.636, 0.204, 0.662, 0.631, 0.998, 0.672, 0.641, + 0.993, 0.681, 0.650, 0.988, 0.691, 0.659, 0.983, 0.700, 0.669, 0.979, + 0.710, 0.678, 0.974, 0.720, 0.687, 0.970, 0.730, 0.696, 0.966, 0.740, + 0.706, 0.963, 0.750, 0.715, 0.960, 0.760, 0.724, 0.957, 0.771, 0.733, + 0.954, 0.781, 0.742, 0.951, 0.791, 0.752, 0.949, 0.802, 0.761, 0.947, + 0.812, 0.770, 0.945, 0.823, 0.779, 0.943, 0.834, 0.789, 0.942, 0.844, + 0.798, 0.941, 0.855, 0.807, 0.940, 0.866, 0.817, 0.939, 0.877, 0.826, + 0.938, 0.887, 0.835, 0.938, 0.898, 0.844, 0.937, 0.909, 0.853, 0.937, + 0.919, 0.862, 0.937, 0.930, 0.870, 0.936, 0.940, 0.878, 0.936, 0.950, + 0.885, 0.934, 0.958, 0.891, 0.932, 0.967, 0.896, 0.928, 0.974, 0.898, + 0.922, 0.979, 0.899, 0.914, 0.982, 0.897, 0.902, 0.984, 0.893, 0.886, + 0.984, 0.887, 0.869, 0.982, 0.880, 0.849, 0.979, 0.872, 0.828, 0.975, + 0.864, 0.807, 0.970, 0.856, 0.785, 0.965, 0.847, 0.762, 0.959, 0.838, + 0.739, 0.954, 0.829, 0.717, 0.948, 0.819, 0.694, 0.943, 0.810, 0.671, + 0.937, 0.801, 0.648, 0.931, 0.792, 0.625, 0.925, 0.782, 0.602, 0.919, + 0.773, 0.579, 0.914, 0.764, 0.556, 0.908, 0.755, 0.533, 0.902, 0.746, + 0.511, 0.897, 0.737, 0.488, 0.891, 0.728, 0.465, 0.886, 0.719, 0.442, + 0.880, 0.710, 0.420, 0.875, 0.701, 0.397, 0.869, 0.692, 0.374, 0.864, + 0.683, 0.350, 0.858, 0.674, 0.327, 0.853, 0.665, 0.303, 0.848, 0.657, + 0.279, 0.842, 0.648, 0.254, 0.837, 0.639, 0.229, 0.832, 0.630, 0.202, + 0.671, 0.625, 0.998, 0.680, 0.635, 0.993, 0.689, 0.644, 0.988, 0.698, + 0.654, 0.983, 0.708, 0.663, 0.978, 0.718, 0.672, 0.974, 0.727, 0.681, + 0.970, 0.737, 0.691, 0.966, 0.747, 0.700, 0.962, 0.757, 0.709, 0.959, + 0.767, 0.718, 0.956, 0.777, 0.727, 0.953, 0.787, 0.736, 0.950, 0.797, + 0.745, 0.948, 0.807, 0.755, 0.946, 0.818, 0.764, 0.944, 0.828, 0.773, + 0.942, 0.839, 0.782, 0.940, 0.849, 0.791, 0.939, 0.859, 0.800, 0.938, + 0.870, 0.809, 0.937, 0.880, 0.818, 0.936, 0.891, 0.827, 0.935, 0.901, + 0.835, 0.934, 0.911, 0.844, 0.933, 0.921, 0.852, 0.932, 0.931, 0.859, + 0.931, 0.941, 0.866, 0.929, 0.950, 0.873, 0.927, 0.958, 0.878, 0.923, + 0.966, 0.881, 0.919, 0.972, 0.883, 0.912, 0.977, 0.883, 0.903, 0.981, + 0.882, 0.891, 0.983, 0.878, 0.876, 0.984, 0.873, 0.859, 0.983, 0.867, + 0.841, 0.980, 0.860, 0.821, 0.977, 0.853, 0.800, 0.974, 0.845, 0.778, + 0.969, 0.836, 0.756, 0.964, 0.828, 0.734, 0.959, 0.819, 0.712, 0.954, + 0.810, 0.689, 0.949, 0.801, 0.666, 0.943, 0.792, 0.644, 0.938, 0.783, + 0.621, 0.932, 0.774, 0.598, 0.927, 0.765, 0.575, 0.921, 0.756, 0.553, + 0.916, 0.747, 0.530, 0.910, 0.738, 0.507, 0.904, 0.729, 0.485, 0.899, + 0.721, 0.462, 0.893, 0.712, 0.439, 0.888, 0.703, 0.417, 0.882, 0.694, + 0.394, 0.877, 0.685, 0.371, 0.871, 0.676, 0.348, 0.866, 0.668, 0.324, + 0.861, 0.659, 0.301, 0.855, 0.650, 0.277, 0.850, 0.641, 0.252, 0.845, + 0.633, 0.226, 0.839, 0.624, 0.200, 0.679, 0.619, 0.998, 0.688, 0.629, + 0.993, 0.697, 0.638, 0.988, 0.706, 0.648, 0.983, 0.715, 0.657, 0.978, + 0.725, 0.666, 0.974, 0.734, 0.675, 0.969, 0.744, 0.684, 0.965, 0.754, + 0.693, 0.962, 0.763, 0.703, 0.958, 0.773, 0.712, 0.955, 0.783, 0.721, + 0.952, 0.793, 0.730, 0.949, 0.803, 0.739, 0.947, 0.813, 0.748, 0.944, + 0.823, 0.757, 0.942, 0.833, 0.766, 0.940, 0.843, 0.774, 0.938, 0.853, + 0.783, 0.937, 0.863, 0.792, 0.935, 0.874, 0.801, 0.934, 0.884, 0.809, + 0.932, 0.894, 0.818, 0.931, 0.904, 0.826, 0.930, 0.913, 0.833, 0.928, + 0.923, 0.841, 0.927, 0.932, 0.848, 0.925, 0.941, 0.854, 0.922, 0.950, + 0.859, 0.919, 0.957, 0.864, 0.915, 0.965, 0.867, 0.909, 0.971, 0.868, + 0.901, 0.976, 0.868, 0.892, 0.980, 0.867, 0.880, 0.982, 0.863, 0.866, + 0.983, 0.859, 0.849, 0.983, 0.854, 0.832, 0.982, 0.847, 0.812, 0.979, + 0.840, 0.792, 0.976, 0.833, 0.771, 0.973, 0.825, 0.750, 0.969, 0.817, + 0.728, 0.964, 0.809, 0.706, 0.959, 0.800, 0.684, 0.954, 0.792, 0.661, + 0.949, 0.783, 0.639, 0.944, 0.774, 0.617, 0.939, 0.766, 0.594, 0.933, + 0.757, 0.572, 0.928, 0.748, 0.549, 0.922, 0.739, 0.527, 0.917, 0.731, + 0.504, 0.911, 0.722, 0.481, 0.906, 0.713, 0.459, 0.901, 0.704, 0.436, + 0.895, 0.695, 0.414, 0.890, 0.687, 0.391, 0.884, 0.678, 0.368, 0.879, + 0.669, 0.345, 0.873, 0.661, 0.322, 0.868, 0.652, 0.298, 0.863, 0.643, + 0.274, 0.857, 0.635, 0.249, 0.852, 0.626, 0.224, 0.846, 0.617, 0.197, + 0.686, 0.613, 0.998, 0.695, 0.622, 0.993, 0.704, 0.632, 0.987, 0.713, + 0.641, 0.982, 0.722, 0.650, 0.977, 0.732, 0.660, 0.973, 0.741, 0.669, + 0.969, 0.750, 0.678, 0.965, 0.760, 0.687, 0.961, 0.769, 0.696, 0.957, + 0.779, 0.705, 0.954, 0.789, 0.714, 0.951, 0.798, 0.723, 0.948, 0.808, + 0.731, 0.945, 0.818, 0.740, 0.943, 0.828, 0.749, 0.940, 0.838, 0.758, + 0.938, 0.847, 0.766, 0.936, 0.857, 0.775, 0.934, 0.867, 0.783, 0.932, + 0.877, 0.792, 0.930, 0.887, 0.800, 0.929, 0.896, 0.808, 0.927, 0.906, + 0.815, 0.925, 0.915, 0.823, 0.923, 0.924, 0.829, 0.921, 0.933, 0.836, + 0.918, 0.942, 0.841, 0.915, 0.950, 0.846, 0.911, 0.957, 0.850, 0.906, + 0.964, 0.852, 0.899, 0.970, 0.853, 0.891, 0.975, 0.853, 0.881, 0.979, + 0.852, 0.869, 0.981, 0.849, 0.855, 0.983, 0.845, 0.840, 0.983, 0.840, + 0.822, 0.982, 0.834, 0.804, 0.981, 0.828, 0.784, 0.978, 0.821, 0.764, + 0.975, 0.814, 0.743, 0.972, 0.806, 0.722, 0.968, 0.798, 0.700, 0.964, + 0.790, 0.678, 0.959, 0.782, 0.656, 0.954, 0.773, 0.634, 0.949, 0.765, + 0.612, 0.944, 0.757, 0.590, 0.939, 0.748, 0.567, 0.934, 0.739, 0.545, + 0.929, 0.731, 0.523, 0.923, 0.722, 0.500, 0.918, 0.714, 0.478, 0.913, + 0.705, 0.456, 0.907, 0.696, 0.433, 0.902, 0.688, 0.411, 0.896, 0.679, + 0.388, 0.891, 0.670, 0.365, 0.886, 0.662, 0.342, 0.880, 0.653, 0.319, + 0.875, 0.645, 0.295, 0.869, 0.636, 0.271, 0.864, 0.628, 0.247, 0.859, + 0.619, 0.221, 0.853, 0.611, 0.195, 0.694, 0.606, 0.998, 0.703, 0.616, + 0.992, 0.711, 0.625, 0.987, 0.720, 0.634, 0.982, 0.729, 0.644, 0.977, + 0.738, 0.653, 0.972, 0.747, 0.662, 0.968, 0.757, 0.671, 0.964, 0.766, + 0.680, 0.960, 0.775, 0.689, 0.956, 0.785, 0.698, 0.953, 0.794, 0.706, + 0.949, 0.804, 0.715, 0.946, 0.813, 0.724, 0.943, 0.823, 0.732, 0.941, + 0.832, 0.741, 0.938, 0.842, 0.749, 0.936, 0.851, 0.758, 0.933, 0.861, + 0.766, 0.931, 0.870, 0.774, 0.929, 0.880, 0.782, 0.927, 0.889, 0.790, + 0.924, 0.899, 0.797, 0.922, 0.908, 0.805, 0.920, 0.917, 0.811, 0.917, + 0.925, 0.818, 0.914, 0.934, 0.823, 0.911, 0.942, 0.828, 0.907, 0.950, + 0.832, 0.902, 0.957, 0.836, 0.896, 0.963, 0.838, 0.889, 0.969, 0.839, + 0.881, 0.974, 0.838, 0.871, 0.978, 0.837, 0.859, 0.980, 0.834, 0.845, + 0.982, 0.831, 0.830, 0.983, 0.826, 0.813, 0.983, 0.821, 0.795, 0.982, + 0.815, 0.776, 0.980, 0.809, 0.757, 0.978, 0.802, 0.736, 0.975, 0.794, + 0.715, 0.971, 0.787, 0.694, 0.967, 0.779, 0.673, 0.963, 0.771, 0.651, + 0.959, 0.763, 0.629, 0.954, 0.755, 0.607, 0.949, 0.747, 0.585, 0.944, + 0.739, 0.563, 0.939, 0.730, 0.541, 0.934, 0.722, 0.519, 0.929, 0.714, + 0.496, 0.924, 0.705, 0.474, 0.919, 0.697, 0.452, 0.913, 0.688, 0.430, + 0.908, 0.680, 0.407, 0.903, 0.671, 0.385, 0.897, 0.663, 0.362, 0.892, + 0.654, 0.339, 0.887, 0.646, 0.316, 0.881, 0.637, 0.293, 0.876, 0.629, + 0.269, 0.870, 0.620, 0.244, 0.865, 0.612, 0.219, 0.860, 0.604, 0.192, + 0.701, 0.599, 0.997, 0.710, 0.609, 0.992, 0.718, 0.618, 0.986, 0.727, + 0.627, 0.981, 0.736, 0.636, 0.976, 0.745, 0.645, 0.971, 0.754, 0.655, + 0.967, 0.763, 0.663, 0.963, 0.772, 0.672, 0.959, 0.781, 0.681, 0.955, + 0.790, 0.690, 0.951, 0.799, 0.699, 0.948, 0.808, 0.707, 0.944, 0.818, + 0.716, 0.941, 0.827, 0.724, 0.938, 0.836, 0.732, 0.935, 0.846, 0.741, + 0.933, 0.855, 0.749, 0.930, 0.864, 0.757, 0.928, 0.874, 0.765, 0.925, + 0.883, 0.772, 0.923, 0.892, 0.780, 0.920, 0.901, 0.787, 0.917, 0.910, + 0.793, 0.914, 0.918, 0.800, 0.911, 0.927, 0.805, 0.908, 0.935, 0.811, + 0.904, 0.942, 0.815, 0.899, 0.950, 0.819, 0.894, 0.956, 0.821, 0.887, + 0.963, 0.823, 0.880, 0.968, 0.824, 0.871, 0.973, 0.824, 0.860, 0.977, + 0.822, 0.849, 0.980, 0.820, 0.835, 0.982, 0.817, 0.820, 0.983, 0.812, + 0.804, 0.983, 0.808, 0.786, 0.983, 0.802, 0.768, 0.981, 0.796, 0.749, + 0.979, 0.789, 0.729, 0.977, 0.783, 0.709, 0.974, 0.776, 0.688, 0.970, + 0.768, 0.667, 0.967, 0.761, 0.645, 0.963, 0.753, 0.624, 0.958, 0.745, + 0.602, 0.954, 0.737, 0.580, 0.949, 0.729, 0.558, 0.944, 0.721, 0.536, + 0.939, 0.713, 0.514, 0.934, 0.704, 0.492, 0.929, 0.696, 0.470, 0.924, + 0.688, 0.448, 0.919, 0.680, 0.426, 0.914, 0.671, 0.404, 0.908, 0.663, + 0.381, 0.903, 0.655, 0.359, 0.898, 0.646, 0.336, 0.893, 0.638, 0.313, + 0.887, 0.629, 0.290, 0.882, 0.621, 0.266, 0.876, 0.613, 0.241, 0.871, + 0.604, 0.216, 0.866, 0.596, 0.189, 0.708, 0.592, 0.997, 0.716, 0.601, + 0.991, 0.725, 0.611, 0.985, 0.733, 0.620, 0.980, 0.742, 0.629, 0.975, + 0.751, 0.638, 0.970, 0.759, 0.647, 0.966, 0.768, 0.656, 0.961, 0.777, + 0.665, 0.957, 0.786, 0.673, 0.953, 0.795, 0.682, 0.949, 0.804, 0.690, + 0.946, 0.813, 0.699, 0.942, 0.822, 0.707, 0.939, 0.831, 0.715, 0.936, + 0.840, 0.723, 0.933, 0.849, 0.731, 0.930, 0.859, 0.739, 0.927, 0.868, + 0.747, 0.924, 0.876, 0.754, 0.921, 0.885, 0.762, 0.918, 0.894, 0.769, + 0.915, 0.903, 0.775, 0.912, 0.911, 0.782, 0.909, 0.920, 0.787, 0.905, + 0.928, 0.793, 0.901, 0.935, 0.798, 0.896, 0.943, 0.802, 0.891, 0.950, + 0.805, 0.885, 0.956, 0.807, 0.878, 0.962, 0.809, 0.870, 0.967, 0.809, + 0.861, 0.972, 0.809, 0.850, 0.976, 0.808, 0.838, 0.979, 0.805, 0.825, + 0.981, 0.802, 0.810, 0.983, 0.799, 0.795, 0.983, 0.794, 0.778, 0.983, + 0.789, 0.760, 0.982, 0.783, 0.741, 0.981, 0.777, 0.721, 0.979, 0.771, + 0.701, 0.976, 0.764, 0.681, 0.973, 0.757, 0.660, 0.970, 0.750, 0.639, + 0.966, 0.742, 0.618, 0.962, 0.735, 0.597, 0.958, 0.727, 0.575, 0.953, + 0.719, 0.553, 0.949, 0.711, 0.532, 0.944, 0.703, 0.510, 0.939, 0.695, + 0.488, 0.934, 0.687, 0.466, 0.929, 0.679, 0.444, 0.924, 0.671, 0.422, + 0.919, 0.663, 0.400, 0.914, 0.654, 0.378, 0.909, 0.646, 0.355, 0.903, + 0.638, 0.333, 0.898, 0.630, 0.310, 0.893, 0.621, 0.287, 0.887, 0.613, + 0.263, 0.882, 0.605, 0.238, 0.877, 0.597, 0.213, 0.871, 0.589, 0.186, + 0.715, 0.584, 0.996, 0.723, 0.593, 0.990, 0.731, 0.603, 0.984, 0.739, + 0.612, 0.979, 0.748, 0.621, 0.974, 0.756, 0.630, 0.969, 0.765, 0.639, + 0.964, 0.774, 0.648, 0.960, 0.782, 0.656, 0.955, 0.791, 0.665, 0.951, + 0.800, 0.673, 0.947, 0.809, 0.682, 0.943, 0.818, 0.690, 0.940, 0.826, + 0.698, 0.936, 0.835, 0.706, 0.933, 0.844, 0.714, 0.930, 0.853, 0.722, + 0.926, 0.862, 0.729, 0.923, 0.871, 0.737, 0.920, 0.879, 0.744, 0.917, + 0.888, 0.751, 0.913, 0.896, 0.758, 0.910, 0.905, 0.764, 0.906, 0.913, + 0.770, 0.903, 0.921, 0.775, 0.898, 0.929, 0.780, 0.894, 0.936, 0.784, + 0.889, 0.943, 0.788, 0.883, 0.950, 0.791, 0.877, 0.956, 0.793, 0.869, + 0.962, 0.794, 0.861, 0.967, 0.795, 0.851, 0.971, 0.794, 0.841, 0.975, + 0.793, 0.829, 0.978, 0.791, 0.815, 0.981, 0.788, 0.801, 0.982, 0.785, + 0.785, 0.983, 0.780, 0.769, 0.983, 0.776, 0.751, 0.983, 0.770, 0.733, + 0.982, 0.764, 0.714, 0.980, 0.758, 0.694, 0.978, 0.752, 0.674, 0.975, + 0.745, 0.654, 0.972, 0.738, 0.633, 0.969, 0.731, 0.612, 0.965, 0.724, + 0.591, 0.961, 0.716, 0.570, 0.957, 0.709, 0.548, 0.953, 0.701, 0.527, + 0.948, 0.693, 0.505, 0.943, 0.685, 0.484, 0.939, 0.678, 0.462, 0.934, + 0.670, 0.440, 0.929, 0.662, 0.418, 0.924, 0.654, 0.396, 0.919, 0.646, + 0.374, 0.914, 0.637, 0.352, 0.908, 0.629, 0.329, 0.903, 0.621, 0.307, + 0.898, 0.613, 0.283, 0.893, 0.605, 0.260, 0.887, 0.597, 0.235, 0.882, + 0.589, 0.210, 0.876, 0.581, 0.183, 0.721, 0.576, 0.995, 0.729, 0.585, + 0.989, 0.737, 0.595, 0.983, 0.745, 0.604, 0.978, 0.754, 0.613, 0.973, + 0.762, 0.622, 0.968, 0.770, 0.631, 0.963, 0.779, 0.639, 0.958, 0.787, + 0.648, 0.954, 0.796, 0.656, 0.949, 0.805, 0.665, 0.945, 0.813, 0.673, + 0.941, 0.822, 0.681, 0.937, 0.830, 0.689, 0.933, 0.839, 0.697, 0.930, + 0.848, 0.704, 0.926, 0.856, 0.712, 0.923, 0.865, 0.719, 0.919, 0.873, + 0.726, 0.916, 0.882, 0.733, 0.912, 0.890, 0.740, 0.908, 0.898, 0.746, + 0.905, 0.906, 0.752, 0.901, 0.914, 0.757, 0.896, 0.922, 0.762, 0.892, + 0.929, 0.767, 0.887, 0.937, 0.771, 0.881, 0.943, 0.774, 0.875, 0.950, + 0.777, 0.868, 0.956, 0.779, 0.860, 0.961, 0.780, 0.851, 0.966, 0.780, + 0.842, 0.971, 0.780, 0.831, 0.975, 0.779, 0.819, 0.978, 0.777, 0.806, + 0.980, 0.774, 0.791, 0.982, 0.771, 0.776, 0.983, 0.767, 0.760, 0.984, + 0.762, 0.742, 0.983, 0.757, 0.725, 0.983, 0.752, 0.706, 0.981, 0.746, + 0.687, 0.979, 0.740, 0.667, 0.977, 0.733, 0.647, 0.974, 0.727, 0.627, + 0.971, 0.720, 0.606, 0.968, 0.713, 0.585, 0.964, 0.706, 0.564, 0.960, + 0.698, 0.543, 0.956, 0.691, 0.522, 0.952, 0.683, 0.501, 0.947, 0.676, + 0.479, 0.943, 0.668, 0.458, 0.938, 0.660, 0.436, 0.933, 0.652, 0.414, + 0.928, 0.644, 0.392, 0.923, 0.636, 0.370, 0.918, 0.629, 0.348, 0.913, + 0.621, 0.326, 0.908, 0.613, 0.303, 0.903, 0.605, 0.280, 0.897, 0.597, + 0.257, 0.892, 0.589, 0.232, 0.887, 0.581, 0.207, 0.881, 0.573, 0.180, + 0.727, 0.568, 0.994, 0.735, 0.577, 0.988, 0.743, 0.586, 0.982, 0.751, + 0.595, 0.977, 0.759, 0.604, 0.971, 0.767, 0.613, 0.966, 0.776, 0.622, + 0.961, 0.784, 0.630, 0.956, 0.792, 0.639, 0.951, 0.801, 0.647, 0.947, + 0.809, 0.655, 0.943, 0.817, 0.663, 0.938, 0.826, 0.671, 0.934, 0.834, + 0.679, 0.930, 0.843, 0.687, 0.927, 0.851, 0.694, 0.923, 0.859, 0.702, + 0.919, 0.868, 0.709, 0.915, 0.876, 0.715, 0.911, 0.884, 0.722, 0.907, + 0.892, 0.728, 0.903, 0.900, 0.734, 0.899, 0.908, 0.740, 0.895, 0.916, + 0.745, 0.890, 0.923, 0.750, 0.885, 0.930, 0.754, 0.879, 0.937, 0.758, + 0.873, 0.944, 0.761, 0.867, 0.950, 0.763, 0.859, 0.956, 0.765, 0.851, + 0.961, 0.766, 0.842, 0.966, 0.766, 0.832, 0.970, 0.766, 0.821, 0.974, + 0.764, 0.809, 0.977, 0.762, 0.796, 0.980, 0.760, 0.782, 0.982, 0.757, + 0.767, 0.983, 0.753, 0.751, 0.984, 0.749, 0.734, 0.984, 0.744, 0.716, + 0.983, 0.739, 0.698, 0.982, 0.733, 0.679, 0.980, 0.727, 0.660, 0.978, + 0.721, 0.640, 0.976, 0.715, 0.620, 0.973, 0.708, 0.600, 0.970, 0.701, + 0.579, 0.967, 0.694, 0.559, 0.963, 0.687, 0.538, 0.959, 0.680, 0.517, + 0.955, 0.673, 0.496, 0.951, 0.665, 0.474, 0.946, 0.658, 0.453, 0.942, + 0.650, 0.432, 0.937, 0.643, 0.410, 0.932, 0.635, 0.388, 0.927, 0.627, + 0.367, 0.922, 0.619, 0.345, 0.917, 0.612, 0.322, 0.912, 0.604, 0.300, + 0.907, 0.596, 0.277, 0.902, 0.588, 0.253, 0.897, 0.580, 0.229, 0.891, + 0.572, 0.204, 0.886, 0.564, 0.177, 0.733, 0.559, 0.993, 0.741, 0.568, + 0.987, 0.749, 0.577, 0.981, 0.757, 0.587, 0.975, 0.765, 0.595, 0.970, + 0.773, 0.604, 0.964, 0.781, 0.613, 0.959, 0.789, 0.621, 0.954, 0.797, + 0.630, 0.949, 0.805, 0.638, 0.945, 0.813, 0.646, 0.940, 0.821, 0.654, + 0.936, 0.830, 0.662, 0.931, 0.838, 0.669, 0.927, 0.846, 0.677, 0.923, + 0.854, 0.684, 0.919, 0.862, 0.691, 0.915, 0.870, 0.698, 0.911, 0.879, + 0.704, 0.907, 0.886, 0.711, 0.902, 0.894, 0.717, 0.898, 0.902, 0.722, + 0.893, 0.910, 0.727, 0.889, 0.917, 0.732, 0.883, 0.924, 0.737, 0.878, + 0.931, 0.741, 0.872, 0.938, 0.744, 0.866, 0.944, 0.747, 0.859, 0.950, + 0.749, 0.851, 0.956, 0.751, 0.842, 0.961, 0.751, 0.833, 0.966, 0.752, + 0.823, 0.970, 0.751, 0.812, 0.974, 0.750, 0.800, 0.977, 0.748, 0.787, + 0.979, 0.746, 0.773, 0.981, 0.743, 0.758, 0.983, 0.739, 0.742, 0.984, + 0.735, 0.725, 0.984, 0.731, 0.708, 0.983, 0.726, 0.690, 0.983, 0.721, + 0.672, 0.981, 0.715, 0.653, 0.980, 0.709, 0.633, 0.977, 0.703, 0.614, + 0.975, 0.697, 0.594, 0.972, 0.690, 0.573, 0.969, 0.683, 0.553, 0.965, + 0.676, 0.532, 0.962, 0.669, 0.512, 0.958, 0.662, 0.491, 0.954, 0.655, + 0.470, 0.949, 0.648, 0.448, 0.945, 0.640, 0.427, 0.941, 0.633, 0.406, + 0.936, 0.625, 0.384, 0.931, 0.618, 0.363, 0.926, 0.610, 0.341, 0.921, + 0.602, 0.319, 0.916, 0.595, 0.296, 0.911, 0.587, 0.273, 0.906, 0.579, + 0.250, 0.901, 0.571, 0.226, 0.896, 0.563, 0.201, 0.890, 0.556, 0.174, + 0.739, 0.550, 0.992, 0.747, 0.559, 0.986, 0.754, 0.568, 0.980, 0.762, + 0.577, 0.974, 0.770, 0.586, 0.968, 0.778, 0.595, 0.962, 0.785, 0.603, + 0.957, 0.793, 0.612, 0.952, 0.801, 0.620, 0.947, 0.809, 0.628, 0.942, + 0.817, 0.636, 0.937, 0.825, 0.644, 0.933, 0.833, 0.651, 0.928, 0.841, + 0.659, 0.924, 0.849, 0.666, 0.919, 0.857, 0.673, 0.915, 0.865, 0.680, + 0.911, 0.873, 0.686, 0.906, 0.881, 0.693, 0.902, 0.889, 0.699, 0.897, + 0.896, 0.705, 0.892, 0.904, 0.710, 0.887, 0.911, 0.715, 0.882, 0.918, + 0.720, 0.877, 0.925, 0.724, 0.871, 0.932, 0.727, 0.865, 0.938, 0.730, + 0.858, 0.944, 0.733, 0.850, 0.950, 0.735, 0.842, 0.956, 0.736, 0.834, + 0.961, 0.737, 0.824, 0.965, 0.737, 0.814, 0.970, 0.737, 0.802, 0.973, + 0.736, 0.790, 0.976, 0.734, 0.777, 0.979, 0.732, 0.763, 0.981, 0.729, + 0.749, 0.982, 0.726, 0.733, 0.983, 0.722, 0.717, 0.984, 0.717, 0.700, + 0.984, 0.713, 0.682, 0.983, 0.708, 0.664, 0.982, 0.702, 0.645, 0.980, + 0.697, 0.626, 0.979, 0.691, 0.607, 0.976, 0.685, 0.587, 0.974, 0.678, + 0.567, 0.971, 0.672, 0.547, 0.967, 0.665, 0.527, 0.964, 0.658, 0.506, + 0.960, 0.651, 0.485, 0.956, 0.644, 0.465, 0.952, 0.637, 0.444, 0.948, + 0.630, 0.423, 0.944, 0.623, 0.401, 0.939, 0.615, 0.380, 0.934, 0.608, + 0.358, 0.930, 0.600, 0.337, 0.925, 0.593, 0.315, 0.920, 0.585, 0.292, + 0.915, 0.578, 0.270, 0.910, 0.570, 0.246, 0.905, 0.562, 0.222, 0.899, + 0.555, 0.197, 0.894, 0.547, 0.171, 0.745, 0.540, 0.991, 0.752, 0.550, + 0.984, 0.760, 0.559, 0.978, 0.767, 0.568, 0.972, 0.775, 0.577, 0.966, + 0.782, 0.585, 0.960, 0.790, 0.594, 0.955, 0.798, 0.602, 0.950, 0.806, + 0.610, 0.944, 0.813, 0.618, 0.939, 0.821, 0.626, 0.934, 0.829, 0.634, + 0.930, 0.837, 0.641, 0.925, 0.845, 0.648, 0.920, 0.852, 0.655, 0.915, + 0.860, 0.662, 0.911, 0.868, 0.669, 0.906, 0.876, 0.675, 0.901, 0.883, + 0.681, 0.897, 0.891, 0.687, 0.892, 0.898, 0.692, 0.887, 0.905, 0.697, + 0.881, 0.912, 0.702, 0.876, 0.919, 0.707, 0.870, 0.926, 0.710, 0.864, + 0.932, 0.714, 0.857, 0.939, 0.717, 0.850, 0.945, 0.719, 0.842, 0.950, + 0.721, 0.834, 0.956, 0.722, 0.825, 0.961, 0.723, 0.815, 0.965, 0.723, + 0.804, 0.969, 0.723, 0.793, 0.973, 0.722, 0.781, 0.976, 0.720, 0.768, + 0.978, 0.718, 0.754, 0.981, 0.715, 0.739, 0.982, 0.712, 0.724, 0.983, + 0.708, 0.708, 0.984, 0.704, 0.691, 0.984, 0.700, 0.674, 0.983, 0.695, + 0.656, 0.982, 0.690, 0.638, 0.981, 0.684, 0.619, 0.979, 0.679, 0.600, + 0.977, 0.673, 0.581, 0.975, 0.666, 0.561, 0.972, 0.660, 0.541, 0.969, + 0.654, 0.521, 0.966, 0.647, 0.501, 0.962, 0.640, 0.480, 0.959, 0.634, + 0.459, 0.955, 0.627, 0.439, 0.951, 0.620, 0.418, 0.946, 0.612, 0.397, + 0.942, 0.605, 0.376, 0.937, 0.598, 0.354, 0.933, 0.591, 0.333, 0.928, + 0.583, 0.311, 0.923, 0.576, 0.289, 0.918, 0.568, 0.266, 0.913, 0.561, + 0.243, 0.908, 0.553, 0.219, 0.903, 0.546, 0.194, 0.898, 0.538, 0.167, + 0.750, 0.531, 0.989, 0.757, 0.540, 0.983, 0.765, 0.549, 0.976, 0.772, + 0.558, 0.970, 0.779, 0.567, 0.964, 0.787, 0.575, 0.958, 0.794, 0.584, + 0.953, 0.802, 0.592, 0.947, 0.810, 0.600, 0.942, 0.817, 0.608, 0.936, + 0.825, 0.615, 0.931, 0.833, 0.623, 0.926, 0.840, 0.630, 0.921, 0.848, + 0.637, 0.916, 0.855, 0.644, 0.911, 0.863, 0.651, 0.907, 0.870, 0.657, + 0.902, 0.878, 0.663, 0.897, 0.885, 0.669, 0.891, 0.893, 0.675, 0.886, + 0.900, 0.680, 0.881, 0.907, 0.685, 0.875, 0.914, 0.689, 0.869, 0.920, + 0.693, 0.863, 0.927, 0.697, 0.857, 0.933, 0.700, 0.850, 0.939, 0.703, + 0.842, 0.945, 0.705, 0.834, 0.950, 0.707, 0.825, 0.956, 0.708, 0.816, + 0.960, 0.709, 0.806, 0.965, 0.709, 0.795, 0.969, 0.708, 0.784, 0.972, + 0.707, 0.772, 0.975, 0.706, 0.759, 0.978, 0.704, 0.745, 0.980, 0.701, + 0.731, 0.982, 0.698, 0.715, 0.983, 0.694, 0.699, 0.984, 0.691, 0.683, + 0.984, 0.686, 0.666, 0.983, 0.682, 0.648, 0.983, 0.677, 0.630, 0.982, + 0.672, 0.612, 0.980, 0.666, 0.593, 0.978, 0.660, 0.574, 0.976, 0.655, + 0.554, 0.973, 0.648, 0.535, 0.971, 0.642, 0.515, 0.967, 0.636, 0.495, + 0.964, 0.629, 0.475, 0.961, 0.623, 0.454, 0.957, 0.616, 0.434, 0.953, + 0.609, 0.413, 0.949, 0.602, 0.392, 0.944, 0.595, 0.371, 0.940, 0.588, + 0.350, 0.936, 0.580, 0.328, 0.931, 0.573, 0.307, 0.926, 0.566, 0.285, + 0.921, 0.559, 0.262, 0.916, 0.551, 0.239, 0.911, 0.544, 0.215, 0.906, + 0.536, 0.190, 0.901, 0.529, 0.164, 0.755, 0.521, 0.988, 0.762, 0.530, + 0.981, 0.770, 0.539, 0.975, 0.777, 0.548, 0.968, 0.784, 0.557, 0.962, + 0.791, 0.565, 0.956, 0.799, 0.573, 0.950, 0.806, 0.582, 0.944, 0.814, + 0.589, 0.939, 0.821, 0.597, 0.933, 0.828, 0.605, 0.928, 0.836, 0.612, + 0.923, 0.843, 0.619, 0.918, 0.851, 0.626, 0.912, 0.858, 0.633, 0.907, + 0.866, 0.639, 0.902, 0.873, 0.645, 0.897, 0.880, 0.651, 0.892, 0.887, + 0.657, 0.886, 0.894, 0.662, 0.881, 0.901, 0.667, 0.875, 0.908, 0.672, + 0.869, 0.915, 0.676, 0.863, 0.921, 0.680, 0.856, 0.928, 0.684, 0.849, + 0.934, 0.687, 0.842, 0.940, 0.689, 0.834, 0.945, 0.691, 0.826, 0.951, + 0.693, 0.817, 0.956, 0.694, 0.807, 0.960, 0.695, 0.797, 0.964, 0.695, + 0.787, 0.968, 0.694, 0.775, 0.972, 0.693, 0.763, 0.975, 0.692, 0.750, + 0.977, 0.690, 0.736, 0.980, 0.687, 0.722, 0.981, 0.684, 0.707, 0.982, + 0.681, 0.691, 0.983, 0.677, 0.675, 0.984, 0.673, 0.658, 0.983, 0.669, + 0.641, 0.983, 0.664, 0.623, 0.982, 0.659, 0.605, 0.980, 0.654, 0.586, + 0.979, 0.648, 0.567, 0.977, 0.642, 0.548, 0.974, 0.637, 0.529, 0.972, + 0.630, 0.509, 0.969, 0.624, 0.489, 0.966, 0.618, 0.469, 0.962, 0.611, + 0.449, 0.959, 0.605, 0.429, 0.955, 0.598, 0.408, 0.951, 0.591, 0.387, + 0.947, 0.584, 0.367, 0.942, 0.577, 0.346, 0.938, 0.570, 0.324, 0.933, + 0.563, 0.303, 0.929, 0.556, 0.281, 0.924, 0.549, 0.258, 0.919, 0.541, + 0.235, 0.914, 0.534, 0.212, 0.909, 0.527, 0.187, 0.904, 0.519, 0.160, + 0.760, 0.510, 0.986, 0.767, 0.520, 0.979, 0.774, 0.529, 0.973, 0.781, + 0.538, 0.966, 0.788, 0.546, 0.960, 0.796, 0.555, 0.954, 0.803, 0.563, + 0.948, 0.810, 0.571, 0.942, 0.817, 0.579, 0.936, 0.825, 0.586, 0.930, + 0.832, 0.594, 0.925, 0.839, 0.601, 0.919, 0.846, 0.608, 0.914, 0.854, + 0.615, 0.908, 0.861, 0.621, 0.903, 0.868, 0.627, 0.897, 0.875, 0.633, + 0.892, 0.882, 0.639, 0.886, 0.889, 0.645, 0.881, 0.896, 0.650, 0.875, + 0.903, 0.655, 0.869, 0.909, 0.659, 0.863, 0.916, 0.663, 0.856, 0.922, + 0.667, 0.849, 0.928, 0.670, 0.842, 0.934, 0.673, 0.834, 0.940, 0.675, + 0.826, 0.945, 0.677, 0.818, 0.951, 0.679, 0.809, 0.955, 0.680, 0.799, + 0.960, 0.680, 0.789, 0.964, 0.680, 0.778, 0.968, 0.680, 0.766, 0.971, + 0.679, 0.754, 0.974, 0.677, 0.741, 0.977, 0.676, 0.727, 0.979, 0.673, + 0.713, 0.981, 0.670, 0.698, 0.982, 0.667, 0.682, 0.983, 0.664, 0.666, + 0.983, 0.660, 0.650, 0.983, 0.655, 0.633, 0.983, 0.651, 0.615, 0.982, + 0.646, 0.597, 0.981, 0.641, 0.579, 0.979, 0.636, 0.560, 0.977, 0.630, + 0.541, 0.975, 0.625, 0.522, 0.973, 0.619, 0.503, 0.970, 0.613, 0.483, + 0.967, 0.606, 0.463, 0.964, 0.600, 0.443, 0.960, 0.594, 0.423, 0.956, + 0.587, 0.403, 0.953, 0.580, 0.383, 0.949, 0.574, 0.362, 0.944, 0.567, + 0.341, 0.940, 0.560, 0.320, 0.936, 0.553, 0.298, 0.931, 0.546, 0.277, + 0.926, 0.539, 0.254, 0.922, 0.532, 0.232, 0.917, 0.524, 0.208, 0.912, + 0.517, 0.183, 0.906, 0.510, 0.156, 0.765, 0.499, 0.985, 0.772, 0.509, + 0.978, 0.779, 0.518, 0.971, 0.786, 0.527, 0.964, 0.793, 0.535, 0.957, + 0.800, 0.544, 0.951, 0.807, 0.552, 0.945, 0.814, 0.560, 0.939, 0.821, + 0.568, 0.933, 0.828, 0.575, 0.927, 0.835, 0.582, 0.921, 0.842, 0.589, + 0.915, 0.849, 0.596, 0.910, 0.856, 0.603, 0.904, 0.863, 0.609, 0.898, + 0.870, 0.615, 0.893, 0.877, 0.621, 0.887, 0.884, 0.627, 0.881, 0.891, + 0.632, 0.875, 0.898, 0.637, 0.869, 0.904, 0.642, 0.863, 0.911, 0.646, + 0.856, 0.917, 0.650, 0.849, 0.923, 0.653, 0.842, 0.929, 0.657, 0.835, + 0.935, 0.659, 0.827, 0.940, 0.662, 0.818, 0.946, 0.663, 0.810, 0.951, + 0.665, 0.800, 0.955, 0.666, 0.790, 0.960, 0.666, 0.780, 0.964, 0.666, + 0.769, 0.967, 0.666, 0.757, 0.971, 0.665, 0.745, 0.974, 0.663, 0.732, + 0.976, 0.662, 0.718, 0.978, 0.659, 0.704, 0.980, 0.657, 0.689, 0.981, + 0.654, 0.674, 0.982, 0.650, 0.658, 0.983, 0.646, 0.642, 0.983, 0.642, + 0.625, 0.983, 0.638, 0.608, 0.982, 0.633, 0.590, 0.981, 0.628, 0.572, + 0.979, 0.623, 0.553, 0.978, 0.618, 0.535, 0.976, 0.612, 0.516, 0.973, + 0.607, 0.497, 0.971, 0.601, 0.477, 0.968, 0.595, 0.458, 0.965, 0.589, + 0.438, 0.961, 0.582, 0.418, 0.958, 0.576, 0.398, 0.954, 0.569, 0.378, + 0.950, 0.563, 0.357, 0.946, 0.556, 0.336, 0.942, 0.549, 0.315, 0.938, + 0.542, 0.294, 0.933, 0.536, 0.273, 0.928, 0.529, 0.250, 0.924, 0.522, + 0.228, 0.919, 0.514, 0.204, 0.914, 0.507, 0.179, 0.909, 0.500, 0.153, + 0.770, 0.488, 0.983, 0.777, 0.498, 0.976, 0.783, 0.507, 0.969, 0.790, + 0.516, 0.962, 0.797, 0.524, 0.955, 0.804, 0.533, 0.948, 0.811, 0.541, + 0.942, 0.817, 0.549, 0.936, 0.824, 0.556, 0.930, 0.831, 0.564, 0.924, + 0.838, 0.571, 0.918, 0.845, 0.578, 0.912, 0.852, 0.584, 0.906, 0.859, + 0.591, 0.900, 0.866, 0.597, 0.894, 0.873, 0.603, 0.888, 0.879, 0.609, + 0.882, 0.886, 0.614, 0.876, 0.893, 0.619, 0.869, 0.899, 0.624, 0.863, + 0.906, 0.629, 0.856, 0.912, 0.633, 0.850, 0.918, 0.637, 0.843, 0.924, + 0.640, 0.835, 0.930, 0.643, 0.827, 0.935, 0.646, 0.819, 0.941, 0.648, + 0.811, 0.946, 0.649, 0.802, 0.951, 0.651, 0.792, 0.955, 0.652, 0.782, + 0.959, 0.652, 0.771, 0.963, 0.652, 0.760, 0.967, 0.652, 0.749, 0.970, + 0.651, 0.736, 0.973, 0.649, 0.723, 0.976, 0.647, 0.710, 0.978, 0.645, + 0.696, 0.980, 0.643, 0.681, 0.981, 0.640, 0.666, 0.982, 0.637, 0.650, + 0.982, 0.633, 0.634, 0.983, 0.629, 0.617, 0.982, 0.625, 0.600, 0.982, + 0.620, 0.582, 0.981, 0.616, 0.565, 0.979, 0.611, 0.546, 0.978, 0.605, + 0.528, 0.976, 0.600, 0.509, 0.974, 0.595, 0.490, 0.971, 0.589, 0.471, + 0.969, 0.583, 0.452, 0.966, 0.577, 0.432, 0.962, 0.571, 0.413, 0.959, + 0.565, 0.393, 0.955, 0.558, 0.373, 0.952, 0.552, 0.352, 0.948, 0.545, + 0.332, 0.943, 0.539, 0.311, 0.939, 0.532, 0.290, 0.935, 0.525, 0.268, + 0.930, 0.518, 0.246, 0.926, 0.511, 0.224, 0.921, 0.504, 0.200, 0.916, + 0.497, 0.175, 0.911, 0.490, 0.149, 0.775, 0.477, 0.981, 0.781, 0.486, + 0.974, 0.788, 0.495, 0.966, 0.794, 0.504, 0.959, 0.801, 0.513, 0.952, + 0.808, 0.521, 0.946, 0.814, 0.529, 0.939, 0.821, 0.537, 0.933, 0.828, + 0.545, 0.926, 0.835, 0.552, 0.920, 0.841, 0.559, 0.914, 0.848, 0.566, + 0.908, 0.855, 0.572, 0.901, 0.862, 0.579, 0.895, 0.868, 0.585, 0.889, + 0.875, 0.591, 0.883, 0.881, 0.596, 0.877, 0.888, 0.601, 0.870, 0.894, + 0.606, 0.864, 0.901, 0.611, 0.857, 0.907, 0.615, 0.850, 0.913, 0.619, + 0.843, 0.919, 0.623, 0.836, 0.925, 0.626, 0.828, 0.930, 0.629, 0.820, + 0.936, 0.632, 0.812, 0.941, 0.634, 0.803, 0.946, 0.635, 0.794, 0.951, + 0.637, 0.784, 0.955, 0.637, 0.774, 0.959, 0.638, 0.763, 0.963, 0.638, + 0.752, 0.967, 0.637, 0.740, 0.970, 0.636, 0.728, 0.973, 0.635, 0.715, + 0.975, 0.633, 0.701, 0.977, 0.631, 0.687, 0.979, 0.629, 0.672, 0.980, + 0.626, 0.657, 0.981, 0.623, 0.642, 0.982, 0.619, 0.626, 0.982, 0.616, + 0.609, 0.982, 0.612, 0.592, 0.981, 0.607, 0.575, 0.981, 0.603, 0.557, + 0.979, 0.598, 0.540, 0.978, 0.593, 0.521, 0.976, 0.588, 0.503, 0.974, + 0.582, 0.484, 0.972, 0.577, 0.465, 0.969, 0.571, 0.446, 0.966, 0.565, + 0.427, 0.963, 0.559, 0.407, 0.960, 0.553, 0.387, 0.956, 0.547, 0.368, + 0.953, 0.541, 0.347, 0.949, 0.534, 0.327, 0.945, 0.528, 0.306, 0.941, + 0.521, 0.285, 0.936, 0.514, 0.264, 0.932, 0.508, 0.242, 0.927, 0.501, + 0.220, 0.922, 0.494, 0.196, 0.918, 0.487, 0.172, 0.913, 0.480, 0.145, + 0.779, 0.465, 0.979, 0.785, 0.474, 0.971, 0.792, 0.484, 0.964, 0.798, + 0.492, 0.957, 0.805, 0.501, 0.950, 0.811, 0.509, 0.943, 0.818, 0.517, + 0.936, 0.824, 0.525, 0.929, 0.831, 0.533, 0.923, 0.838, 0.540, 0.916, + 0.844, 0.547, 0.910, 0.851, 0.554, 0.904, 0.857, 0.560, 0.897, 0.864, + 0.566, 0.891, 0.870, 0.572, 0.884, 0.877, 0.578, 0.878, 0.883, 0.583, + 0.871, 0.890, 0.589, 0.865, 0.896, 0.593, 0.858, 0.902, 0.598, 0.851, + 0.908, 0.602, 0.844, 0.914, 0.606, 0.837, 0.920, 0.609, 0.829, 0.925, + 0.613, 0.821, 0.931, 0.615, 0.813, 0.936, 0.618, 0.804, 0.941, 0.620, + 0.795, 0.946, 0.621, 0.786, 0.950, 0.622, 0.776, 0.955, 0.623, 0.765, + 0.959, 0.624, 0.755, 0.963, 0.624, 0.743, 0.966, 0.623, 0.731, 0.969, + 0.622, 0.719, 0.972, 0.621, 0.706, 0.974, 0.619, 0.693, 0.976, 0.617, + 0.679, 0.978, 0.615, 0.664, 0.979, 0.612, 0.649, 0.980, 0.609, 0.634, + 0.981, 0.606, 0.618, 0.981, 0.602, 0.601, 0.981, 0.598, 0.585, 0.981, + 0.594, 0.568, 0.980, 0.590, 0.550, 0.979, 0.585, 0.533, 0.978, 0.580, + 0.515, 0.976, 0.575, 0.496, 0.974, 0.570, 0.478, 0.972, 0.565, 0.459, + 0.969, 0.559, 0.440, 0.967, 0.553, 0.421, 0.964, 0.547, 0.402, 0.960, + 0.542, 0.382, 0.957, 0.535, 0.362, 0.953, 0.529, 0.342, 0.950, 0.523, + 0.322, 0.946, 0.517, 0.302, 0.942, 0.510, 0.281, 0.937, 0.504, 0.260, + 0.933, 0.497, 0.238, 0.929, 0.490, 0.216, 0.924, 0.484, 0.192, 0.919, + 0.477, 0.168, 0.914, 0.470, 0.141, 0.783, 0.453, 0.977, 0.789, 0.462, + 0.969, 0.796, 0.471, 0.962, 0.802, 0.480, 0.954, 0.808, 0.489, 0.947, + 0.815, 0.497, 0.940, 0.821, 0.505, 0.933, 0.828, 0.513, 0.926, 0.834, + 0.520, 0.919, 0.840, 0.527, 0.913, 0.847, 0.534, 0.906, 0.853, 0.541, + 0.899, 0.860, 0.548, 0.893, 0.866, 0.554, 0.886, 0.873, 0.560, 0.880, + 0.879, 0.565, 0.873, 0.885, 0.570, 0.866, 0.891, 0.575, 0.859, 0.897, + 0.580, 0.852, 0.903, 0.585, 0.845, 0.909, 0.589, 0.838, 0.915, 0.592, + 0.830, 0.921, 0.596, 0.822, 0.926, 0.599, 0.814, 0.931, 0.601, 0.805, + 0.936, 0.604, 0.797, 0.941, 0.606, 0.787, 0.946, 0.607, 0.778, 0.950, + 0.608, 0.768, 0.955, 0.609, 0.757, 0.958, 0.609, 0.746, 0.962, 0.609, + 0.735, 0.965, 0.609, 0.723, 0.968, 0.608, 0.710, 0.971, 0.607, 0.698, + 0.974, 0.605, 0.684, 0.976, 0.603, 0.670, 0.977, 0.601, 0.656, 0.979, + 0.598, 0.641, 0.980, 0.596, 0.626, 0.980, 0.592, 0.610, 0.981, 0.589, + 0.594, 0.981, 0.585, 0.577, 0.980, 0.581, 0.560, 0.980, 0.577, 0.543, + 0.979, 0.572, 0.526, 0.977, 0.568, 0.508, 0.976, 0.563, 0.490, 0.974, + 0.558, 0.471, 0.972, 0.552, 0.453, 0.969, 0.547, 0.434, 0.967, 0.541, + 0.415, 0.964, 0.536, 0.396, 0.961, 0.530, 0.377, 0.958, 0.524, 0.357, + 0.954, 0.518, 0.337, 0.950, 0.512, 0.317, 0.947, 0.505, 0.297, 0.943, + 0.499, 0.276, 0.938, 0.493, 0.255, 0.934, 0.486, 0.234, 0.930, 0.480, + 0.211, 0.925, 0.473, 0.188, 0.920, 0.466, 0.164, 0.916, 0.459, 0.137, + 0.787, 0.440, 0.975, 0.793, 0.450, 0.967, 0.799, 0.459, 0.959, 0.806, + 0.468, 0.952, 0.812, 0.476, 0.944, 0.818, 0.485, 0.937, 0.824, 0.493, + 0.930, 0.831, 0.500, 0.923, 0.837, 0.508, 0.916, 0.843, 0.515, 0.909, + 0.850, 0.522, 0.902, 0.856, 0.528, 0.895, 0.862, 0.535, 0.888, 0.868, + 0.541, 0.881, 0.874, 0.547, 0.875, 0.881, 0.552, 0.868, 0.887, 0.557, + 0.861, 0.893, 0.562, 0.853, 0.899, 0.567, 0.846, 0.904, 0.571, 0.839, + 0.910, 0.575, 0.831, 0.916, 0.579, 0.823, 0.921, 0.582, 0.815, 0.926, + 0.585, 0.807, 0.932, 0.587, 0.798, 0.937, 0.590, 0.789, 0.941, 0.591, + 0.780, 0.946, 0.593, 0.770, 0.950, 0.594, 0.760, 0.954, 0.595, 0.749, + 0.958, 0.595, 0.738, 0.962, 0.595, 0.727, 0.965, 0.595, 0.715, 0.968, + 0.594, 0.702, 0.970, 0.593, 0.689, 0.973, 0.591, 0.676, 0.975, 0.589, + 0.662, 0.976, 0.587, 0.648, 0.978, 0.585, 0.633, 0.979, 0.582, 0.618, + 0.980, 0.579, 0.602, 0.980, 0.575, 0.586, 0.980, 0.572, 0.570, 0.980, + 0.568, 0.553, 0.979, 0.564, 0.536, 0.978, 0.559, 0.518, 0.977, 0.555, + 0.501, 0.975, 0.550, 0.483, 0.974, 0.545, 0.465, 0.972, 0.540, 0.447, + 0.969, 0.535, 0.428, 0.967, 0.529, 0.409, 0.964, 0.524, 0.390, 0.961, + 0.518, 0.371, 0.958, 0.512, 0.352, 0.954, 0.506, 0.332, 0.951, 0.500, + 0.312, 0.947, 0.494, 0.292, 0.943, 0.488, 0.272, 0.939, 0.481, 0.251, + 0.935, 0.475, 0.229, 0.931, 0.469, 0.207, 0.926, 0.462, 0.184, 0.921, + 0.455, 0.159, 0.917, 0.449, 0.133, 0.791, 0.427, 0.973, 0.797, 0.437, + 0.964, 0.803, 0.446, 0.957, 0.809, 0.455, 0.949, 0.815, 0.464, 0.941, + 0.821, 0.472, 0.934, 0.827, 0.480, 0.926, 0.834, 0.488, 0.919, 0.840, + 0.495, 0.912, 0.846, 0.502, 0.905, 0.852, 0.509, 0.898, 0.858, 0.515, + 0.891, 0.864, 0.522, 0.884, 0.870, 0.528, 0.877, 0.876, 0.533, 0.870, + 0.882, 0.539, 0.862, 0.888, 0.544, 0.855, 0.894, 0.549, 0.848, 0.900, + 0.553, 0.840, 0.906, 0.557, 0.833, 0.911, 0.561, 0.825, 0.916, 0.565, + 0.817, 0.922, 0.568, 0.808, 0.927, 0.571, 0.800, 0.932, 0.573, 0.791, + 0.937, 0.575, 0.782, 0.941, 0.577, 0.772, 0.946, 0.579, 0.762, 0.950, + 0.580, 0.752, 0.954, 0.580, 0.741, 0.958, 0.581, 0.730, 0.961, 0.581, + 0.718, 0.964, 0.580, 0.706, 0.967, 0.580, 0.694, 0.970, 0.578, 0.681, + 0.972, 0.577, 0.667, 0.974, 0.575, 0.654, 0.976, 0.573, 0.639, 0.977, + 0.571, 0.625, 0.978, 0.568, 0.610, 0.979, 0.565, 0.594, 0.979, 0.562, + 0.578, 0.979, 0.558, 0.562, 0.979, 0.554, 0.545, 0.978, 0.550, 0.529, + 0.977, 0.546, 0.511, 0.976, 0.542, 0.494, 0.975, 0.537, 0.476, 0.973, + 0.532, 0.458, 0.971, 0.527, 0.440, 0.969, 0.522, 0.422, 0.967, 0.517, + 0.403, 0.964, 0.511, 0.385, 0.961, 0.506, 0.366, 0.958, 0.500, 0.347, + 0.955, 0.494, 0.327, 0.951, 0.488, 0.307, 0.947, 0.482, 0.287, 0.944, + 0.476, 0.267, 0.940, 0.470, 0.246, 0.935, 0.464, 0.225, 0.931, 0.458, + 0.203, 0.927, 0.451, 0.180, 0.922, 0.445, 0.155, 0.917, 0.438, 0.129, + 0.795, 0.413, 0.970, 0.801, 0.423, 0.962, 0.807, 0.433, 0.954, 0.813, + 0.442, 0.946, 0.818, 0.450, 0.938, 0.824, 0.459, 0.930, 0.830, 0.467, + 0.923, 0.836, 0.474, 0.915, 0.842, 0.482, 0.908, 0.848, 0.489, 0.901, + 0.854, 0.496, 0.894, 0.860, 0.502, 0.886, 0.866, 0.508, 0.879, 0.872, + 0.514, 0.872, 0.878, 0.520, 0.864, 0.884, 0.525, 0.857, 0.890, 0.530, + 0.850, 0.895, 0.535, 0.842, 0.901, 0.539, 0.834, 0.906, 0.543, 0.826, + 0.912, 0.547, 0.818, 0.917, 0.551, 0.810, 0.922, 0.554, 0.801, 0.927, + 0.557, 0.793, 0.932, 0.559, 0.783, 0.937, 0.561, 0.774, 0.941, 0.563, + 0.764, 0.946, 0.564, 0.754, 0.950, 0.565, 0.744, 0.953, 0.566, 0.733, + 0.957, 0.566, 0.722, 0.960, 0.566, 0.710, 0.963, 0.566, 0.698, 0.966, + 0.565, 0.685, 0.969, 0.564, 0.673, 0.971, 0.563, 0.659, 0.973, 0.561, + 0.645, 0.975, 0.559, 0.631, 0.976, 0.557, 0.617, 0.977, 0.554, 0.602, + 0.978, 0.551, 0.586, 0.978, 0.548, 0.571, 0.978, 0.545, 0.554, 0.978, + 0.541, 0.538, 0.977, 0.537, 0.521, 0.977, 0.533, 0.504, 0.976, 0.529, + 0.487, 0.974, 0.524, 0.470, 0.973, 0.519, 0.452, 0.971, 0.515, 0.434, + 0.969, 0.510, 0.416, 0.966, 0.504, 0.398, 0.964, 0.499, 0.379, 0.961, + 0.494, 0.360, 0.958, 0.488, 0.341, 0.955, 0.482, 0.322, 0.951, 0.477, + 0.302, 0.948, 0.471, 0.283, 0.944, 0.465, 0.262, 0.940, 0.459, 0.242, + 0.936, 0.452, 0.220, 0.932, 0.446, 0.198, 0.927, 0.440, 0.175, 0.923, + 0.434, 0.151, 0.918, 0.427, 0.124, 0.799, 0.399, 0.968, 0.804, 0.409, + 0.959, 0.810, 0.419, 0.951, 0.816, 0.428, 0.943, 0.822, 0.437, 0.935, + 0.827, 0.445, 0.927, 0.833, 0.453, 0.919, 0.839, 0.461, 0.912, 0.845, + 0.468, 0.904, 0.851, 0.475, 0.897, 0.857, 0.482, 0.889, 0.862, 0.489, + 0.882, 0.868, 0.495, 0.874, 0.874, 0.501, 0.867, 0.880, 0.506, 0.859, + 0.885, 0.511, 0.852, 0.891, 0.516, 0.844, 0.897, 0.521, 0.836, 0.902, + 0.525, 0.828, 0.907, 0.529, 0.820, 0.913, 0.533, 0.812, 0.918, 0.537, + 0.803, 0.923, 0.540, 0.794, 0.928, 0.542, 0.785, 0.932, 0.545, 0.776, + 0.937, 0.547, 0.767, 0.941, 0.549, 0.757, 0.945, 0.550, 0.747, 0.949, + 0.551, 0.736, 0.953, 0.552, 0.725, 0.956, 0.552, 0.714, 0.960, 0.552, + 0.702, 0.963, 0.552, 0.690, 0.965, 0.551, 0.677, 0.968, 0.550, 0.664, + 0.970, 0.549, 0.651, 0.972, 0.547, 0.637, 0.974, 0.545, 0.623, 0.975, + 0.543, 0.609, 0.976, 0.540, 0.594, 0.977, 0.537, 0.578, 0.977, 0.534, + 0.563, 0.977, 0.531, 0.547, 0.977, 0.527, 0.531, 0.976, 0.524, 0.514, + 0.976, 0.520, 0.497, 0.975, 0.516, 0.480, 0.973, 0.511, 0.463, 0.972, + 0.507, 0.446, 0.970, 0.502, 0.428, 0.968, 0.497, 0.410, 0.966, 0.492, + 0.392, 0.963, 0.487, 0.373, 0.960, 0.481, 0.355, 0.957, 0.476, 0.336, + 0.954, 0.470, 0.317, 0.951, 0.465, 0.297, 0.947, 0.459, 0.278, 0.944, + 0.453, 0.258, 0.940, 0.447, 0.237, 0.936, 0.441, 0.216, 0.932, 0.435, + 0.194, 0.927, 0.429, 0.171, 0.923, 0.422, 0.147, 0.918, 0.416, 0.120, + 0.802, 0.385, 0.965, 0.808, 0.395, 0.957, 0.813, 0.405, 0.948, 0.819, + 0.414, 0.940, 0.825, 0.423, 0.932, 0.830, 0.431, 0.924, 0.836, 0.439, + 0.916, 0.842, 0.447, 0.908, 0.847, 0.454, 0.900, 0.853, 0.462, 0.892, + 0.859, 0.468, 0.885, 0.864, 0.475, 0.877, 0.870, 0.481, 0.869, 0.876, + 0.487, 0.862, 0.881, 0.492, 0.854, 0.887, 0.498, 0.846, 0.892, 0.502, + 0.838, 0.898, 0.507, 0.830, 0.903, 0.511, 0.822, 0.908, 0.515, 0.814, + 0.913, 0.519, 0.805, 0.918, 0.522, 0.797, 0.923, 0.525, 0.788, 0.928, + 0.528, 0.778, 0.932, 0.530, 0.769, 0.937, 0.532, 0.759, 0.941, 0.534, + 0.749, 0.945, 0.535, 0.739, 0.949, 0.536, 0.728, 0.952, 0.537, 0.717, + 0.956, 0.537, 0.706, 0.959, 0.537, 0.694, 0.962, 0.537, 0.682, 0.964, + 0.536, 0.669, 0.967, 0.536, 0.656, 0.969, 0.534, 0.643, 0.971, 0.533, + 0.629, 0.972, 0.531, 0.615, 0.974, 0.529, 0.601, 0.975, 0.526, 0.586, + 0.975, 0.523, 0.571, 0.976, 0.521, 0.555, 0.976, 0.517, 0.540, 0.976, + 0.514, 0.523, 0.975, 0.510, 0.507, 0.975, 0.506, 0.490, 0.974, 0.502, + 0.474, 0.972, 0.498, 0.456, 0.971, 0.494, 0.439, 0.969, 0.489, 0.421, + 0.967, 0.484, 0.404, 0.965, 0.479, 0.386, 0.963, 0.474, 0.367, 0.960, + 0.469, 0.349, 0.957, 0.464, 0.330, 0.954, 0.458, 0.311, 0.951, 0.453, + 0.292, 0.947, 0.447, 0.273, 0.944, 0.441, 0.253, 0.940, 0.435, 0.232, + 0.936, 0.429, 0.211, 0.932, 0.423, 0.190, 0.927, 0.417, 0.167, 0.923, + 0.411, 0.142, 0.919, 0.405, 0.116, 0.806, 0.370, 0.963, 0.811, 0.380, + 0.954, 0.816, 0.390, 0.945, 0.822, 0.399, 0.937, 0.827, 0.408, 0.929, + 0.833, 0.417, 0.920, 0.838, 0.425, 0.912, 0.844, 0.433, 0.904, 0.850, + 0.440, 0.896, 0.855, 0.447, 0.888, 0.861, 0.454, 0.880, 0.866, 0.461, + 0.872, 0.872, 0.467, 0.865, 0.877, 0.473, 0.857, 0.883, 0.478, 0.849, + 0.888, 0.483, 0.841, 0.893, 0.488, 0.833, 0.899, 0.493, 0.824, 0.904, + 0.497, 0.816, 0.909, 0.501, 0.807, 0.914, 0.505, 0.799, 0.919, 0.508, + 0.790, 0.923, 0.511, 0.781, 0.928, 0.514, 0.771, 0.932, 0.516, 0.762, + 0.937, 0.518, 0.752, 0.941, 0.520, 0.742, 0.945, 0.521, 0.731, 0.948, + 0.522, 0.720, 0.952, 0.523, 0.709, 0.955, 0.523, 0.698, 0.958, 0.523, + 0.686, 0.961, 0.523, 0.674, 0.964, 0.522, 0.661, 0.966, 0.521, 0.648, + 0.968, 0.520, 0.635, 0.970, 0.518, 0.621, 0.971, 0.517, 0.607, 0.972, + 0.514, 0.593, 0.973, 0.512, 0.578, 0.974, 0.509, 0.563, 0.975, 0.507, + 0.548, 0.975, 0.503, 0.532, 0.975, 0.500, 0.516, 0.974, 0.497, 0.500, + 0.974, 0.493, 0.483, 0.973, 0.489, 0.467, 0.971, 0.485, 0.450, 0.970, + 0.480, 0.433, 0.968, 0.476, 0.415, 0.966, 0.471, 0.397, 0.964, 0.466, + 0.380, 0.962, 0.461, 0.362, 0.959, 0.456, 0.343, 0.956, 0.451, 0.325, + 0.953, 0.446, 0.306, 0.950, 0.440, 0.287, 0.947, 0.435, 0.268, 0.943, + 0.429, 0.248, 0.939, 0.423, 0.228, 0.936, 0.417, 0.207, 0.931, 0.411, + 0.185, 0.927, 0.405, 0.162, 0.923, 0.399, 0.138, 0.918, 0.393, 0.111, + 0.809, 0.354, 0.960, 0.814, 0.364, 0.951, 0.819, 0.375, 0.942, 0.825, + 0.384, 0.934, 0.830, 0.393, 0.925, 0.835, 0.402, 0.917, 0.841, 0.410, + 0.908, 0.846, 0.418, 0.900, 0.852, 0.426, 0.892, 0.857, 0.433, 0.884, + 0.863, 0.440, 0.876, 0.868, 0.446, 0.868, 0.873, 0.452, 0.860, 0.879, + 0.458, 0.852, 0.884, 0.464, 0.843, 0.889, 0.469, 0.835, 0.894, 0.474, + 0.827, 0.899, 0.478, 0.818, 0.904, 0.483, 0.810, 0.909, 0.486, 0.801, + 0.914, 0.490, 0.792, 0.919, 0.493, 0.783, 0.923, 0.496, 0.774, 0.928, + 0.499, 0.764, 0.932, 0.501, 0.755, 0.936, 0.503, 0.745, 0.940, 0.505, + 0.734, 0.944, 0.506, 0.724, 0.948, 0.507, 0.713, 0.951, 0.508, 0.701, + 0.954, 0.508, 0.690, 0.957, 0.508, 0.678, 0.960, 0.508, 0.666, 0.962, + 0.507, 0.653, 0.965, 0.507, 0.640, 0.967, 0.505, 0.627, 0.968, 0.504, + 0.613, 0.970, 0.502, 0.599, 0.971, 0.500, 0.585, 0.972, 0.498, 0.570, + 0.973, 0.495, 0.555, 0.973, 0.493, 0.540, 0.973, 0.490, 0.525, 0.973, + 0.486, 0.509, 0.973, 0.483, 0.493, 0.972, 0.479, 0.476, 0.971, 0.475, + 0.460, 0.970, 0.471, 0.443, 0.969, 0.467, 0.426, 0.967, 0.463, 0.409, + 0.965, 0.458, 0.391, 0.963, 0.453, 0.374, 0.961, 0.449, 0.356, 0.958, + 0.444, 0.338, 0.956, 0.438, 0.319, 0.953, 0.433, 0.301, 0.949, 0.428, + 0.282, 0.946, 0.422, 0.263, 0.943, 0.417, 0.243, 0.939, 0.411, 0.223, + 0.935, 0.405, 0.202, 0.931, 0.399, 0.181, 0.927, 0.393, 0.158, 0.923, + 0.387, 0.134, 0.918, 0.381, 0.107, + ]).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)) + +cmaps = { + "BiPeak": SegmentedBivarColormap( + BiPeak, 256, "square", (.5, .5), name="BiPeak"), + "BiOrangeBlue": SegmentedBivarColormap( + BiOrangeBlue, 256, "square", (0, 0), name="BiOrangeBlue"), + "BiCone": SegmentedBivarColormap(BiPeak, 256, "circle", (.5, .5), name="BiCone"), +} diff --git a/lib/matplotlib/_cm_listed.py b/lib/matplotlib/_cm_listed.py index a331ad74a5f0..b90e0a23acb0 100644 --- a/lib/matplotlib/_cm_listed.py +++ b/lib/matplotlib/_cm_listed.py @@ -2057,6 +2057,779 @@ [0.49321, 0.01963, 0.00955], [0.47960, 0.01583, 0.01055]] +_berlin_data = [ + [0.62108, 0.69018, 0.99951], + [0.61216, 0.68923, 0.99537], + [0.6032, 0.68825, 0.99124], + [0.5942, 0.68726, 0.98709], + [0.58517, 0.68625, 0.98292], + [0.57609, 0.68522, 0.97873], + [0.56696, 0.68417, 0.97452], + [0.55779, 0.6831, 0.97029], + [0.54859, 0.68199, 0.96602], + [0.53933, 0.68086, 0.9617], + [0.53003, 0.67969, 0.95735], + [0.52069, 0.67848, 0.95294], + [0.51129, 0.67723, 0.94847], + [0.50186, 0.67591, 0.94392], + [0.49237, 0.67453, 0.9393], + [0.48283, 0.67308, 0.93457], + [0.47324, 0.67153, 0.92975], + [0.46361, 0.6699, 0.92481], + [0.45393, 0.66815, 0.91974], + [0.44421, 0.66628, 0.91452], + [0.43444, 0.66427, 0.90914], + [0.42465, 0.66212, 0.90359], + [0.41482, 0.65979, 0.89785], + [0.40498, 0.65729, 0.89191], + [0.39514, 0.65458, 0.88575], + [0.3853, 0.65167, 0.87937], + [0.37549, 0.64854, 0.87276], + [0.36574, 0.64516, 0.8659], + [0.35606, 0.64155, 0.8588], + [0.34645, 0.63769, 0.85145], + [0.33698, 0.63357, 0.84386], + [0.32764, 0.62919, 0.83602], + [0.31849, 0.62455, 0.82794], + [0.30954, 0.61966, 0.81963], + [0.30078, 0.6145, 0.81111], + [0.29231, 0.60911, 0.80238], + [0.2841, 0.60348, 0.79347], + [0.27621, 0.59763, 0.78439], + [0.26859, 0.59158, 0.77514], + [0.26131, 0.58534, 0.76578], + [0.25437, 0.57891, 0.7563], + [0.24775, 0.57233, 0.74672], + [0.24146, 0.5656, 0.73707], + [0.23552, 0.55875, 0.72735], + [0.22984, 0.5518, 0.7176], + [0.2245, 0.54475, 0.7078], + [0.21948, 0.53763, 0.698], + [0.21469, 0.53043, 0.68819], + [0.21017, 0.52319, 0.67838], + [0.20589, 0.5159, 0.66858], + [0.20177, 0.5086, 0.65879], + [0.19788, 0.50126, 0.64903], + [0.19417, 0.4939, 0.63929], + [0.19056, 0.48654, 0.62957], + [0.18711, 0.47918, 0.6199], + [0.18375, 0.47183, 0.61024], + [0.1805, 0.46447, 0.60062], + [0.17737, 0.45712, 0.59104], + [0.17426, 0.44979, 0.58148], + [0.17122, 0.44247, 0.57197], + [0.16824, 0.43517, 0.56249], + [0.16529, 0.42788, 0.55302], + [0.16244, 0.42061, 0.5436], + [0.15954, 0.41337, 0.53421], + [0.15674, 0.40615, 0.52486], + [0.15391, 0.39893, 0.51552], + [0.15112, 0.39176, 0.50623], + [0.14835, 0.38459, 0.49697], + [0.14564, 0.37746, 0.48775], + [0.14288, 0.37034, 0.47854], + [0.14014, 0.36326, 0.46939], + [0.13747, 0.3562, 0.46024], + [0.13478, 0.34916, 0.45115], + [0.13208, 0.34215, 0.44209], + [0.1294, 0.33517, 0.43304], + [0.12674, 0.3282, 0.42404], + [0.12409, 0.32126, 0.41507], + [0.12146, 0.31435, 0.40614], + [0.1189, 0.30746, 0.39723], + [0.11632, 0.30061, 0.38838], + [0.11373, 0.29378, 0.37955], + [0.11119, 0.28698, 0.37075], + [0.10861, 0.28022, 0.362], + [0.10616, 0.2735, 0.35328], + [0.10367, 0.26678, 0.34459], + [0.10118, 0.26011, 0.33595], + [0.098776, 0.25347, 0.32734], + [0.096347, 0.24685, 0.31878], + [0.094059, 0.24026, 0.31027], + [0.091788, 0.23373, 0.30176], + [0.089506, 0.22725, 0.29332], + [0.087341, 0.2208, 0.28491], + [0.085142, 0.21436, 0.27658], + [0.083069, 0.20798, 0.26825], + [0.081098, 0.20163, 0.25999], + [0.07913, 0.19536, 0.25178], + [0.077286, 0.18914, 0.24359], + [0.075571, 0.18294, 0.2355], + [0.073993, 0.17683, 0.22743], + [0.07241, 0.17079, 0.21943], + [0.071045, 0.1648, 0.2115], + [0.069767, 0.1589, 0.20363], + [0.068618, 0.15304, 0.19582], + [0.06756, 0.14732, 0.18812], + [0.066665, 0.14167, 0.18045], + [0.065923, 0.13608, 0.17292], + [0.065339, 0.1307, 0.16546], + [0.064911, 0.12535, 0.15817], + [0.064636, 0.12013, 0.15095], + [0.064517, 0.11507, 0.14389], + [0.064554, 0.11022, 0.13696], + [0.064749, 0.10543, 0.13023], + [0.0651, 0.10085, 0.12357], + [0.065383, 0.096469, 0.11717], + [0.065574, 0.092338, 0.11101], + [0.065892, 0.088201, 0.10498], + [0.066388, 0.084134, 0.099288], + [0.067108, 0.080051, 0.093829], + [0.068193, 0.076099, 0.08847], + [0.06972, 0.072283, 0.083025], + [0.071639, 0.068654, 0.077544], + [0.073978, 0.065058, 0.07211], + [0.076596, 0.061657, 0.066651], + [0.079637, 0.05855, 0.061133], + [0.082963, 0.055666, 0.055745], + [0.086537, 0.052997, 0.050336], + [0.090315, 0.050699, 0.04504], + [0.09426, 0.048753, 0.039773], + [0.098319, 0.047041, 0.034683], + [0.10246, 0.045624, 0.030074], + [0.10673, 0.044705, 0.026012], + [0.11099, 0.043972, 0.022379], + [0.11524, 0.043596, 0.01915], + [0.11955, 0.043567, 0.016299], + [0.12381, 0.043861, 0.013797], + [0.1281, 0.044459, 0.011588], + [0.13232, 0.045229, 0.0095315], + [0.13645, 0.046164, 0.0078947], + [0.14063, 0.047374, 0.006502], + [0.14488, 0.048634, 0.0053266], + [0.14923, 0.049836, 0.0043455], + [0.15369, 0.050997, 0.0035374], + [0.15831, 0.05213, 0.0028824], + [0.16301, 0.053218, 0.0023628], + [0.16781, 0.05424, 0.0019629], + [0.17274, 0.055172, 0.001669], + [0.1778, 0.056018, 0.0014692], + [0.18286, 0.05682, 0.0013401], + [0.18806, 0.057574, 0.0012617], + [0.19323, 0.058514, 0.0012261], + [0.19846, 0.05955, 0.0012271], + [0.20378, 0.060501, 0.0012601], + [0.20909, 0.061486, 0.0013221], + [0.21447, 0.06271, 0.0014116], + [0.2199, 0.063823, 0.0015287], + [0.22535, 0.065027, 0.0016748], + [0.23086, 0.066297, 0.0018529], + [0.23642, 0.067645, 0.0020675], + [0.24202, 0.069092, 0.0023247], + [0.24768, 0.070458, 0.0026319], + [0.25339, 0.071986, 0.0029984], + [0.25918, 0.07364, 0.003435], + [0.265, 0.075237, 0.0039545], + [0.27093, 0.076965, 0.004571], + [0.27693, 0.078822, 0.0053006], + [0.28302, 0.080819, 0.0061608], + [0.2892, 0.082879, 0.0071713], + [0.29547, 0.085075, 0.0083494], + [0.30186, 0.08746, 0.0097258], + [0.30839, 0.089912, 0.011455], + [0.31502, 0.09253, 0.013324], + [0.32181, 0.095392, 0.015413], + [0.32874, 0.098396, 0.01778], + [0.3358, 0.10158, 0.020449], + [0.34304, 0.10498, 0.02344], + [0.35041, 0.10864, 0.026771], + [0.35795, 0.11256, 0.030456], + [0.36563, 0.11666, 0.034571], + [0.37347, 0.12097, 0.039115], + [0.38146, 0.12561, 0.043693], + [0.38958, 0.13046, 0.048471], + [0.39785, 0.13547, 0.053136], + [0.40622, 0.1408, 0.057848], + [0.41469, 0.14627, 0.062715], + [0.42323, 0.15198, 0.067685], + [0.43184, 0.15791, 0.073044], + [0.44044, 0.16403, 0.07862], + [0.44909, 0.17027, 0.084644], + [0.4577, 0.17667, 0.090869], + [0.46631, 0.18321, 0.097335], + [0.4749, 0.18989, 0.10406], + [0.48342, 0.19668, 0.11104], + [0.49191, 0.20352, 0.11819], + [0.50032, 0.21043, 0.1255], + [0.50869, 0.21742, 0.13298], + [0.51698, 0.22443, 0.14062], + [0.5252, 0.23154, 0.14835], + [0.53335, 0.23862, 0.15626], + [0.54144, 0.24575, 0.16423], + [0.54948, 0.25292, 0.17226], + [0.55746, 0.26009, 0.1804], + [0.56538, 0.26726, 0.18864], + [0.57327, 0.27446, 0.19692], + [0.58111, 0.28167, 0.20524], + [0.58892, 0.28889, 0.21362], + [0.59672, 0.29611, 0.22205], + [0.60448, 0.30335, 0.23053], + [0.61223, 0.31062, 0.23905], + [0.61998, 0.31787, 0.24762], + [0.62771, 0.32513, 0.25619], + [0.63544, 0.33244, 0.26481], + [0.64317, 0.33975, 0.27349], + [0.65092, 0.34706, 0.28218], + [0.65866, 0.3544, 0.29089], + [0.66642, 0.36175, 0.29964], + [0.67419, 0.36912, 0.30842], + [0.68198, 0.37652, 0.31722], + [0.68978, 0.38392, 0.32604], + [0.6976, 0.39135, 0.33493], + [0.70543, 0.39879, 0.3438], + [0.71329, 0.40627, 0.35272], + [0.72116, 0.41376, 0.36166], + [0.72905, 0.42126, 0.37062], + [0.73697, 0.4288, 0.37962], + [0.7449, 0.43635, 0.38864], + [0.75285, 0.44392, 0.39768], + [0.76083, 0.45151, 0.40675], + [0.76882, 0.45912, 0.41584], + [0.77684, 0.46676, 0.42496], + [0.78488, 0.47441, 0.43409], + [0.79293, 0.48208, 0.44327], + [0.80101, 0.48976, 0.45246], + [0.80911, 0.49749, 0.46167], + [0.81722, 0.50521, 0.47091], + [0.82536, 0.51296, 0.48017], + [0.83352, 0.52073, 0.48945], + [0.84169, 0.52853, 0.49876], + [0.84988, 0.53634, 0.5081], + [0.85809, 0.54416, 0.51745], + [0.86632, 0.55201, 0.52683], + [0.87457, 0.55988, 0.53622], + [0.88283, 0.56776, 0.54564], + [0.89111, 0.57567, 0.55508], + [0.89941, 0.58358, 0.56455], + [0.90772, 0.59153, 0.57404], + [0.91603, 0.59949, 0.58355], + [0.92437, 0.60747, 0.59309], + [0.93271, 0.61546, 0.60265], + [0.94108, 0.62348, 0.61223], + [0.94945, 0.63151, 0.62183], + [0.95783, 0.63956, 0.63147], + [0.96622, 0.64763, 0.64111], + [0.97462, 0.65572, 0.65079], + [0.98303, 0.66382, 0.66049], + [0.99145, 0.67194, 0.67022], + [0.99987, 0.68007, 0.67995]] + +_managua_data = [ + [1, 0.81263, 0.40424], + [0.99516, 0.80455, 0.40155], + [0.99024, 0.79649, 0.39888], + [0.98532, 0.78848, 0.39622], + [0.98041, 0.7805, 0.39356], + [0.97551, 0.77257, 0.39093], + [0.97062, 0.76468, 0.3883], + [0.96573, 0.75684, 0.38568], + [0.96087, 0.74904, 0.3831], + [0.95601, 0.74129, 0.38052], + [0.95116, 0.7336, 0.37795], + [0.94631, 0.72595, 0.37539], + [0.94149, 0.71835, 0.37286], + [0.93667, 0.7108, 0.37034], + [0.93186, 0.7033, 0.36784], + [0.92706, 0.69585, 0.36536], + [0.92228, 0.68845, 0.36289], + [0.9175, 0.68109, 0.36042], + [0.91273, 0.67379, 0.358], + [0.90797, 0.66653, 0.35558], + [0.90321, 0.65932, 0.35316], + [0.89846, 0.65216, 0.35078], + [0.89372, 0.64503, 0.34839], + [0.88899, 0.63796, 0.34601], + [0.88426, 0.63093, 0.34367], + [0.87953, 0.62395, 0.34134], + [0.87481, 0.617, 0.33902], + [0.87009, 0.61009, 0.3367], + [0.86538, 0.60323, 0.33442], + [0.86067, 0.59641, 0.33213], + [0.85597, 0.58963, 0.32987], + [0.85125, 0.5829, 0.3276], + [0.84655, 0.57621, 0.32536], + [0.84185, 0.56954, 0.32315], + [0.83714, 0.56294, 0.32094], + [0.83243, 0.55635, 0.31874], + [0.82772, 0.54983, 0.31656], + [0.82301, 0.54333, 0.31438], + [0.81829, 0.53688, 0.31222], + [0.81357, 0.53046, 0.3101], + [0.80886, 0.52408, 0.30796], + [0.80413, 0.51775, 0.30587], + [0.7994, 0.51145, 0.30375], + [0.79466, 0.50519, 0.30167], + [0.78991, 0.49898, 0.29962], + [0.78516, 0.4928, 0.29757], + [0.7804, 0.48668, 0.29553], + [0.77564, 0.48058, 0.29351], + [0.77086, 0.47454, 0.29153], + [0.76608, 0.46853, 0.28954], + [0.76128, 0.46255, 0.28756], + [0.75647, 0.45663, 0.28561], + [0.75166, 0.45075, 0.28369], + [0.74682, 0.44491, 0.28178], + [0.74197, 0.4391, 0.27988], + [0.73711, 0.43333, 0.27801], + [0.73223, 0.42762, 0.27616], + [0.72732, 0.42192, 0.2743], + [0.72239, 0.41628, 0.27247], + [0.71746, 0.41067, 0.27069], + [0.71247, 0.40508, 0.26891], + [0.70747, 0.39952, 0.26712], + [0.70244, 0.39401, 0.26538], + [0.69737, 0.38852, 0.26367], + [0.69227, 0.38306, 0.26194], + [0.68712, 0.37761, 0.26025], + [0.68193, 0.37219, 0.25857], + [0.67671, 0.3668, 0.25692], + [0.67143, 0.36142, 0.25529], + [0.6661, 0.35607, 0.25367], + [0.66071, 0.35073, 0.25208], + [0.65528, 0.34539, 0.25049], + [0.6498, 0.34009, 0.24895], + [0.64425, 0.3348, 0.24742], + [0.63866, 0.32953, 0.2459], + [0.633, 0.32425, 0.24442], + [0.62729, 0.31901, 0.24298], + [0.62152, 0.3138, 0.24157], + [0.6157, 0.3086, 0.24017], + [0.60983, 0.30341, 0.23881], + [0.60391, 0.29826, 0.23752], + [0.59793, 0.29314, 0.23623], + [0.59191, 0.28805, 0.235], + [0.58585, 0.28302, 0.23377], + [0.57974, 0.27799, 0.23263], + [0.57359, 0.27302, 0.23155], + [0.56741, 0.26808, 0.23047], + [0.5612, 0.26321, 0.22948], + [0.55496, 0.25837, 0.22857], + [0.54871, 0.25361, 0.22769], + [0.54243, 0.24891, 0.22689], + [0.53614, 0.24424, 0.22616], + [0.52984, 0.23968, 0.22548], + [0.52354, 0.2352, 0.22487], + [0.51724, 0.23076, 0.22436], + [0.51094, 0.22643, 0.22395], + [0.50467, 0.22217, 0.22363], + [0.49841, 0.21802, 0.22339], + [0.49217, 0.21397, 0.22325], + [0.48595, 0.21, 0.22321], + [0.47979, 0.20618, 0.22328], + [0.47364, 0.20242, 0.22345], + [0.46756, 0.1988, 0.22373], + [0.46152, 0.19532, 0.22413], + [0.45554, 0.19195, 0.22465], + [0.44962, 0.18873, 0.22534], + [0.44377, 0.18566, 0.22616], + [0.43799, 0.18266, 0.22708], + [0.43229, 0.17987, 0.22817], + [0.42665, 0.17723, 0.22938], + [0.42111, 0.17474, 0.23077], + [0.41567, 0.17238, 0.23232], + [0.41033, 0.17023, 0.23401], + [0.40507, 0.16822, 0.2359], + [0.39992, 0.1664, 0.23794], + [0.39489, 0.16475, 0.24014], + [0.38996, 0.16331, 0.24254], + [0.38515, 0.16203, 0.24512], + [0.38046, 0.16093, 0.24792], + [0.37589, 0.16, 0.25087], + [0.37143, 0.15932, 0.25403], + [0.36711, 0.15883, 0.25738], + [0.36292, 0.15853, 0.26092], + [0.35885, 0.15843, 0.26466], + [0.35494, 0.15853, 0.26862], + [0.35114, 0.15882, 0.27276], + [0.34748, 0.15931, 0.27711], + [0.34394, 0.15999, 0.28164], + [0.34056, 0.16094, 0.28636], + [0.33731, 0.16207, 0.29131], + [0.3342, 0.16338, 0.29642], + [0.33121, 0.16486, 0.3017], + [0.32837, 0.16658, 0.30719], + [0.32565, 0.16847, 0.31284], + [0.3231, 0.17056, 0.31867], + [0.32066, 0.17283, 0.32465], + [0.31834, 0.1753, 0.33079], + [0.31616, 0.17797, 0.3371], + [0.3141, 0.18074, 0.34354], + [0.31216, 0.18373, 0.35011], + [0.31038, 0.1869, 0.35682], + [0.3087, 0.19021, 0.36363], + [0.30712, 0.1937, 0.37056], + [0.3057, 0.19732, 0.3776], + [0.30435, 0.20106, 0.38473], + [0.30314, 0.205, 0.39195], + [0.30204, 0.20905, 0.39924], + [0.30106, 0.21323, 0.40661], + [0.30019, 0.21756, 0.41404], + [0.29944, 0.22198, 0.42151], + [0.29878, 0.22656, 0.42904], + [0.29822, 0.23122, 0.4366], + [0.29778, 0.23599, 0.44419], + [0.29745, 0.24085, 0.45179], + [0.29721, 0.24582, 0.45941], + [0.29708, 0.2509, 0.46703], + [0.29704, 0.25603, 0.47465], + [0.2971, 0.26127, 0.48225], + [0.29726, 0.26658, 0.48983], + [0.2975, 0.27194, 0.4974], + [0.29784, 0.27741, 0.50493], + [0.29828, 0.28292, 0.51242], + [0.29881, 0.28847, 0.51987], + [0.29943, 0.29408, 0.52728], + [0.30012, 0.29976, 0.53463], + [0.3009, 0.30548, 0.54191], + [0.30176, 0.31122, 0.54915], + [0.30271, 0.317, 0.5563], + [0.30373, 0.32283, 0.56339], + [0.30483, 0.32866, 0.5704], + [0.30601, 0.33454, 0.57733], + [0.30722, 0.34042, 0.58418], + [0.30853, 0.34631, 0.59095], + [0.30989, 0.35224, 0.59763], + [0.3113, 0.35817, 0.60423], + [0.31277, 0.3641, 0.61073], + [0.31431, 0.37005, 0.61715], + [0.3159, 0.376, 0.62347], + [0.31752, 0.38195, 0.62969], + [0.3192, 0.3879, 0.63583], + [0.32092, 0.39385, 0.64188], + [0.32268, 0.39979, 0.64783], + [0.32446, 0.40575, 0.6537], + [0.3263, 0.41168, 0.65948], + [0.32817, 0.41763, 0.66517], + [0.33008, 0.42355, 0.67079], + [0.33201, 0.4295, 0.67632], + [0.33398, 0.43544, 0.68176], + [0.33596, 0.44137, 0.68715], + [0.33798, 0.44731, 0.69246], + [0.34003, 0.45327, 0.69769], + [0.3421, 0.45923, 0.70288], + [0.34419, 0.4652, 0.70799], + [0.34631, 0.4712, 0.71306], + [0.34847, 0.4772, 0.71808], + [0.35064, 0.48323, 0.72305], + [0.35283, 0.48928, 0.72798], + [0.35506, 0.49537, 0.73288], + [0.3573, 0.50149, 0.73773], + [0.35955, 0.50763, 0.74256], + [0.36185, 0.51381, 0.74736], + [0.36414, 0.52001, 0.75213], + [0.36649, 0.52627, 0.75689], + [0.36884, 0.53256, 0.76162], + [0.37119, 0.53889, 0.76633], + [0.37359, 0.54525, 0.77103], + [0.376, 0.55166, 0.77571], + [0.37842, 0.55809, 0.78037], + [0.38087, 0.56458, 0.78503], + [0.38333, 0.5711, 0.78966], + [0.38579, 0.57766, 0.79429], + [0.38828, 0.58426, 0.7989], + [0.39078, 0.59088, 0.8035], + [0.39329, 0.59755, 0.8081], + [0.39582, 0.60426, 0.81268], + [0.39835, 0.61099, 0.81725], + [0.4009, 0.61774, 0.82182], + [0.40344, 0.62454, 0.82637], + [0.406, 0.63137, 0.83092], + [0.40856, 0.63822, 0.83546], + [0.41114, 0.6451, 0.83999], + [0.41372, 0.65202, 0.84451], + [0.41631, 0.65896, 0.84903], + [0.4189, 0.66593, 0.85354], + [0.42149, 0.67294, 0.85805], + [0.4241, 0.67996, 0.86256], + [0.42671, 0.68702, 0.86705], + [0.42932, 0.69411, 0.87156], + [0.43195, 0.70123, 0.87606], + [0.43457, 0.70839, 0.88056], + [0.4372, 0.71557, 0.88506], + [0.43983, 0.72278, 0.88956], + [0.44248, 0.73004, 0.89407], + [0.44512, 0.73732, 0.89858], + [0.44776, 0.74464, 0.9031], + [0.45042, 0.752, 0.90763], + [0.45308, 0.75939, 0.91216], + [0.45574, 0.76682, 0.9167], + [0.45841, 0.77429, 0.92124], + [0.46109, 0.78181, 0.9258], + [0.46377, 0.78936, 0.93036], + [0.46645, 0.79694, 0.93494], + [0.46914, 0.80458, 0.93952], + [0.47183, 0.81224, 0.94412], + [0.47453, 0.81995, 0.94872], + [0.47721, 0.8277, 0.95334], + [0.47992, 0.83549, 0.95796], + [0.48261, 0.84331, 0.96259], + [0.4853, 0.85117, 0.96722], + [0.48801, 0.85906, 0.97186], + [0.49071, 0.86699, 0.97651], + [0.49339, 0.87495, 0.98116], + [0.49607, 0.88294, 0.98581], + [0.49877, 0.89096, 0.99047], + [0.50144, 0.89901, 0.99512], + [0.50411, 0.90708, 0.99978]] + +_vanimo_data = [ + [1, 0.80346, 0.99215], + [0.99397, 0.79197, 0.98374], + [0.98791, 0.78052, 0.97535], + [0.98185, 0.7691, 0.96699], + [0.97578, 0.75774, 0.95867], + [0.96971, 0.74643, 0.95037], + [0.96363, 0.73517, 0.94211], + [0.95755, 0.72397, 0.93389], + [0.95147, 0.71284, 0.9257], + [0.94539, 0.70177, 0.91756], + [0.93931, 0.69077, 0.90945], + [0.93322, 0.67984, 0.90137], + [0.92713, 0.66899, 0.89334], + [0.92104, 0.65821, 0.88534], + [0.91495, 0.64751, 0.87738], + [0.90886, 0.63689, 0.86946], + [0.90276, 0.62634, 0.86158], + [0.89666, 0.61588, 0.85372], + [0.89055, 0.60551, 0.84591], + [0.88444, 0.59522, 0.83813], + [0.87831, 0.58503, 0.83039], + [0.87219, 0.57491, 0.82268], + [0.86605, 0.5649, 0.815], + [0.8599, 0.55499, 0.80736], + [0.85373, 0.54517, 0.79974], + [0.84756, 0.53544, 0.79216], + [0.84138, 0.52583, 0.78461], + [0.83517, 0.5163, 0.77709], + [0.82896, 0.5069, 0.76959], + [0.82272, 0.49761, 0.76212], + [0.81647, 0.48841, 0.75469], + [0.81018, 0.47934, 0.74728], + [0.80389, 0.47038, 0.7399], + [0.79757, 0.46154, 0.73255], + [0.79123, 0.45283, 0.72522], + [0.78487, 0.44424, 0.71792], + [0.77847, 0.43578, 0.71064], + [0.77206, 0.42745, 0.70339], + [0.76562, 0.41925, 0.69617], + [0.75914, 0.41118, 0.68897], + [0.75264, 0.40327, 0.68179], + [0.74612, 0.39549, 0.67465], + [0.73957, 0.38783, 0.66752], + [0.73297, 0.38034, 0.66041], + [0.72634, 0.37297, 0.65331], + [0.71967, 0.36575, 0.64623], + [0.71293, 0.35864, 0.63915], + [0.70615, 0.35166, 0.63206], + [0.69929, 0.34481, 0.62496], + [0.69236, 0.33804, 0.61782], + [0.68532, 0.33137, 0.61064], + [0.67817, 0.32479, 0.6034], + [0.67091, 0.3183, 0.59609], + [0.66351, 0.31184, 0.5887], + [0.65598, 0.30549, 0.58123], + [0.64828, 0.29917, 0.57366], + [0.64045, 0.29289, 0.56599], + [0.63245, 0.28667, 0.55822], + [0.6243, 0.28051, 0.55035], + [0.61598, 0.27442, 0.54237], + [0.60752, 0.26838, 0.53428], + [0.59889, 0.2624, 0.5261], + [0.59012, 0.25648, 0.51782], + [0.5812, 0.25063, 0.50944], + [0.57214, 0.24483, 0.50097], + [0.56294, 0.23914, 0.4924], + [0.55359, 0.23348, 0.48376], + [0.54413, 0.22795, 0.47505], + [0.53454, 0.22245, 0.46623], + [0.52483, 0.21706, 0.45736], + [0.51501, 0.21174, 0.44843], + [0.50508, 0.20651, 0.43942], + [0.49507, 0.20131, 0.43036], + [0.48495, 0.19628, 0.42125], + [0.47476, 0.19128, 0.4121], + [0.4645, 0.18639, 0.4029], + [0.45415, 0.18157, 0.39367], + [0.44376, 0.17688, 0.38441], + [0.43331, 0.17225, 0.37513], + [0.42282, 0.16773, 0.36585], + [0.41232, 0.16332, 0.35655], + [0.40178, 0.15897, 0.34726], + [0.39125, 0.15471, 0.33796], + [0.38071, 0.15058, 0.32869], + [0.37017, 0.14651, 0.31945], + [0.35969, 0.14258, 0.31025], + [0.34923, 0.13872, 0.30106], + [0.33883, 0.13499, 0.29196], + [0.32849, 0.13133, 0.28293], + [0.31824, 0.12778, 0.27396], + [0.30808, 0.12431, 0.26508], + [0.29805, 0.12097, 0.25631], + [0.28815, 0.11778, 0.24768], + [0.27841, 0.11462, 0.23916], + [0.26885, 0.11169, 0.23079], + [0.25946, 0.10877, 0.22259], + [0.25025, 0.10605, 0.21455], + [0.24131, 0.10341, 0.20673], + [0.23258, 0.10086, 0.19905], + [0.2241, 0.098494, 0.19163], + [0.21593, 0.096182, 0.18443], + [0.20799, 0.094098, 0.17748], + [0.20032, 0.092102, 0.17072], + [0.19299, 0.09021, 0.16425], + [0.18596, 0.088461, 0.15799], + [0.17918, 0.086861, 0.15197], + [0.17272, 0.08531, 0.14623], + [0.16658, 0.084017, 0.14075], + [0.1607, 0.082745, 0.13546], + [0.15515, 0.081683, 0.13049], + [0.1499, 0.080653, 0.1257], + [0.14493, 0.07978, 0.12112], + [0.1402, 0.079037, 0.11685], + [0.13578, 0.078426, 0.11282], + [0.13168, 0.077944, 0.10894], + [0.12782, 0.077586, 0.10529], + [0.12422, 0.077332, 0.1019], + [0.12091, 0.077161, 0.098724], + [0.11793, 0.077088, 0.095739], + [0.11512, 0.077124, 0.092921], + [0.11267, 0.077278, 0.090344], + [0.11042, 0.077557, 0.087858], + [0.10835, 0.077968, 0.085431], + [0.10665, 0.078516, 0.083233], + [0.105, 0.079207, 0.081185], + [0.10368, 0.080048, 0.079202], + [0.10245, 0.081036, 0.077408], + [0.10143, 0.082173, 0.075793], + [0.1006, 0.083343, 0.074344], + [0.099957, 0.084733, 0.073021], + [0.099492, 0.086174, 0.071799], + [0.099204, 0.087868, 0.070716], + [0.099092, 0.089631, 0.069813], + [0.099154, 0.091582, 0.069047], + [0.099384, 0.093597, 0.068337], + [0.099759, 0.095871, 0.067776], + [0.10029, 0.098368, 0.067351], + [0.10099, 0.101, 0.067056], + [0.10185, 0.1039, 0.066891], + [0.1029, 0.10702, 0.066853], + [0.10407, 0.11031, 0.066942], + [0.10543, 0.1138, 0.067155], + [0.10701, 0.1175, 0.067485], + [0.10866, 0.12142, 0.067929], + [0.11059, 0.12561, 0.06849], + [0.11265, 0.12998, 0.069162], + [0.11483, 0.13453, 0.069842], + [0.11725, 0.13923, 0.07061], + [0.11985, 0.14422, 0.071528], + [0.12259, 0.14937, 0.072403], + [0.12558, 0.15467, 0.073463], + [0.12867, 0.16015, 0.074429], + [0.13196, 0.16584, 0.075451], + [0.1354, 0.17169, 0.076499], + [0.13898, 0.17771, 0.077615], + [0.14273, 0.18382, 0.078814], + [0.14658, 0.1901, 0.080098], + [0.15058, 0.19654, 0.081473], + [0.15468, 0.20304, 0.08282], + [0.15891, 0.20968, 0.084315], + [0.16324, 0.21644, 0.085726], + [0.16764, 0.22326, 0.087378], + [0.17214, 0.23015, 0.088955], + [0.17673, 0.23717, 0.090617], + [0.18139, 0.24418, 0.092314], + [0.18615, 0.25132, 0.094071], + [0.19092, 0.25846, 0.095839], + [0.19578, 0.26567, 0.097702], + [0.20067, 0.2729, 0.099539], + [0.20564, 0.28016, 0.10144], + [0.21062, 0.28744, 0.10342], + [0.21565, 0.29475, 0.10534], + [0.22072, 0.30207, 0.10737], + [0.22579, 0.30942, 0.10942], + [0.23087, 0.31675, 0.11146], + [0.236, 0.32407, 0.11354], + [0.24112, 0.3314, 0.11563], + [0.24625, 0.33874, 0.11774], + [0.25142, 0.34605, 0.11988], + [0.25656, 0.35337, 0.12202], + [0.26171, 0.36065, 0.12422], + [0.26686, 0.36793, 0.12645], + [0.272, 0.37519, 0.12865], + [0.27717, 0.38242, 0.13092], + [0.28231, 0.38964, 0.13316], + [0.28741, 0.39682, 0.13541], + [0.29253, 0.40398, 0.13773], + [0.29763, 0.41111, 0.13998], + [0.30271, 0.4182, 0.14232], + [0.30778, 0.42527, 0.14466], + [0.31283, 0.43231, 0.14699], + [0.31787, 0.43929, 0.14937], + [0.32289, 0.44625, 0.15173], + [0.32787, 0.45318, 0.15414], + [0.33286, 0.46006, 0.1566], + [0.33781, 0.46693, 0.15904], + [0.34276, 0.47374, 0.16155], + [0.34769, 0.48054, 0.16407], + [0.3526, 0.48733, 0.16661], + [0.35753, 0.4941, 0.16923], + [0.36245, 0.50086, 0.17185], + [0.36738, 0.50764, 0.17458], + [0.37234, 0.51443, 0.17738], + [0.37735, 0.52125, 0.18022], + [0.38238, 0.52812, 0.18318], + [0.38746, 0.53505, 0.18626], + [0.39261, 0.54204, 0.18942], + [0.39783, 0.54911, 0.19272], + [0.40311, 0.55624, 0.19616], + [0.40846, 0.56348, 0.1997], + [0.4139, 0.57078, 0.20345], + [0.41942, 0.57819, 0.20734], + [0.42503, 0.5857, 0.2114], + [0.43071, 0.59329, 0.21565], + [0.43649, 0.60098, 0.22009], + [0.44237, 0.60878, 0.2247], + [0.44833, 0.61667, 0.22956], + [0.45439, 0.62465, 0.23468], + [0.46053, 0.63274, 0.23997], + [0.46679, 0.64092, 0.24553], + [0.47313, 0.64921, 0.25138], + [0.47959, 0.6576, 0.25745], + [0.48612, 0.66608, 0.26382], + [0.49277, 0.67466, 0.27047], + [0.49951, 0.68335, 0.2774], + [0.50636, 0.69213, 0.28464], + [0.51331, 0.70101, 0.2922], + [0.52035, 0.70998, 0.30008], + [0.5275, 0.71905, 0.30828], + [0.53474, 0.72821, 0.31682], + [0.54207, 0.73747, 0.32567], + [0.5495, 0.74682, 0.33491], + [0.55702, 0.75625, 0.34443], + [0.56461, 0.76577, 0.35434], + [0.5723, 0.77537, 0.36457], + [0.58006, 0.78506, 0.37515], + [0.58789, 0.79482, 0.38607], + [0.59581, 0.80465, 0.39734], + [0.60379, 0.81455, 0.40894], + [0.61182, 0.82453, 0.42086], + [0.61991, 0.83457, 0.43311], + [0.62805, 0.84467, 0.44566], + [0.63623, 0.85482, 0.45852], + [0.64445, 0.86503, 0.47168], + [0.6527, 0.8753, 0.48511], + [0.66099, 0.88562, 0.49882], + [0.6693, 0.89599, 0.51278], + [0.67763, 0.90641, 0.52699], + [0.68597, 0.91687, 0.54141], + [0.69432, 0.92738, 0.55605], + [0.70269, 0.93794, 0.5709], + [0.71107, 0.94855, 0.58593], + [0.71945, 0.9592, 0.60112], + [0.72782, 0.96989, 0.61646], + [0.7362, 0.98063, 0.63191], + [0.74458, 0.99141, 0.64748]] cmaps = { name: ListedColormap(data, name=name) for name, data in [ @@ -2068,4 +2841,7 @@ ('twilight', _twilight_data), ('twilight_shifted', _twilight_shifted_data), ('turbo', _turbo_data), + ('berlin', _berlin_data), + ('managua', _managua_data), + ('vanimo', _vanimo_data), ]} diff --git a/lib/matplotlib/_cm_multivar.py b/lib/matplotlib/_cm_multivar.py new file mode 100644 index 000000000000..610d7c40935b --- /dev/null +++ b/lib/matplotlib/_cm_multivar.py @@ -0,0 +1,166 @@ +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-28 + +from .colors import LinearSegmentedColormap, MultivarColormap +import matplotlib as mpl +_LUTSIZE = mpl.rcParams['image.lut'] + +_2VarAddA0_data = [[0.000, 0.000, 0.000], + [0.020, 0.026, 0.031], + [0.049, 0.068, 0.085], + [0.075, 0.107, 0.135], + [0.097, 0.144, 0.183], + [0.116, 0.178, 0.231], + [0.133, 0.212, 0.279], + [0.148, 0.244, 0.326], + [0.161, 0.276, 0.374], + [0.173, 0.308, 0.422], + [0.182, 0.339, 0.471], + [0.190, 0.370, 0.521], + [0.197, 0.400, 0.572], + [0.201, 0.431, 0.623], + [0.204, 0.461, 0.675], + [0.204, 0.491, 0.728], + [0.202, 0.520, 0.783], + [0.197, 0.549, 0.838], + [0.187, 0.577, 0.895]] + +_2VarAddA1_data = [[0.000, 0.000, 0.000], + [0.030, 0.023, 0.018], + [0.079, 0.060, 0.043], + [0.125, 0.093, 0.065], + [0.170, 0.123, 0.083], + [0.213, 0.151, 0.098], + [0.255, 0.177, 0.110], + [0.298, 0.202, 0.120], + [0.341, 0.226, 0.128], + [0.384, 0.249, 0.134], + [0.427, 0.271, 0.138], + [0.472, 0.292, 0.141], + [0.517, 0.313, 0.142], + [0.563, 0.333, 0.141], + [0.610, 0.353, 0.139], + [0.658, 0.372, 0.134], + [0.708, 0.390, 0.127], + [0.759, 0.407, 0.118], + [0.813, 0.423, 0.105]] + +_2VarSubA0_data = [[1.000, 1.000, 1.000], + [0.959, 0.973, 0.986], + [0.916, 0.948, 0.974], + [0.874, 0.923, 0.965], + [0.832, 0.899, 0.956], + [0.790, 0.875, 0.948], + [0.748, 0.852, 0.940], + [0.707, 0.829, 0.934], + [0.665, 0.806, 0.927], + [0.624, 0.784, 0.921], + [0.583, 0.762, 0.916], + [0.541, 0.740, 0.910], + [0.500, 0.718, 0.905], + [0.457, 0.697, 0.901], + [0.414, 0.675, 0.896], + [0.369, 0.652, 0.892], + [0.320, 0.629, 0.888], + [0.266, 0.604, 0.884], + [0.199, 0.574, 0.881]] + +_2VarSubA1_data = [[1.000, 1.000, 1.000], + [0.982, 0.967, 0.955], + [0.966, 0.935, 0.908], + [0.951, 0.902, 0.860], + [0.937, 0.870, 0.813], + [0.923, 0.838, 0.765], + [0.910, 0.807, 0.718], + [0.898, 0.776, 0.671], + [0.886, 0.745, 0.624], + [0.874, 0.714, 0.577], + [0.862, 0.683, 0.530], + [0.851, 0.653, 0.483], + [0.841, 0.622, 0.435], + [0.831, 0.592, 0.388], + [0.822, 0.561, 0.340], + [0.813, 0.530, 0.290], + [0.806, 0.498, 0.239], + [0.802, 0.464, 0.184], + [0.801, 0.426, 0.119]] + +_3VarAddA0_data = [[0.000, 0.000, 0.000], + [0.018, 0.023, 0.028], + [0.040, 0.056, 0.071], + [0.059, 0.087, 0.110], + [0.074, 0.114, 0.147], + [0.086, 0.139, 0.183], + [0.095, 0.163, 0.219], + [0.101, 0.187, 0.255], + [0.105, 0.209, 0.290], + [0.107, 0.230, 0.326], + [0.105, 0.251, 0.362], + [0.101, 0.271, 0.398], + [0.091, 0.291, 0.434], + [0.075, 0.309, 0.471], + [0.046, 0.325, 0.507], + [0.021, 0.341, 0.546], + [0.021, 0.363, 0.584], + [0.022, 0.385, 0.622], + [0.023, 0.408, 0.661]] + +_3VarAddA1_data = [[0.000, 0.000, 0.000], + [0.020, 0.024, 0.016], + [0.047, 0.058, 0.034], + [0.072, 0.088, 0.048], + [0.093, 0.116, 0.059], + [0.113, 0.142, 0.067], + [0.131, 0.167, 0.071], + [0.149, 0.190, 0.074], + [0.166, 0.213, 0.074], + [0.182, 0.235, 0.072], + [0.198, 0.256, 0.068], + [0.215, 0.276, 0.061], + [0.232, 0.296, 0.051], + [0.249, 0.314, 0.037], + [0.270, 0.330, 0.018], + [0.288, 0.347, 0.000], + [0.302, 0.369, 0.000], + [0.315, 0.391, 0.000], + [0.328, 0.414, 0.000]] + +_3VarAddA2_data = [[0.000, 0.000, 0.000], + [0.029, 0.020, 0.023], + [0.072, 0.045, 0.055], + [0.111, 0.067, 0.084], + [0.148, 0.085, 0.109], + [0.184, 0.101, 0.133], + [0.219, 0.115, 0.155], + [0.254, 0.127, 0.176], + [0.289, 0.138, 0.195], + [0.323, 0.147, 0.214], + [0.358, 0.155, 0.232], + [0.393, 0.161, 0.250], + [0.429, 0.166, 0.267], + [0.467, 0.169, 0.283], + [0.507, 0.168, 0.298], + [0.546, 0.168, 0.313], + [0.580, 0.172, 0.328], + [0.615, 0.175, 0.341], + [0.649, 0.178, 0.355]] + +cmaps = { + name: LinearSegmentedColormap.from_list(name, data, _LUTSIZE) for name, data in [ + ('2VarAddA0', _2VarAddA0_data), + ('2VarAddA1', _2VarAddA1_data), + ('2VarSubA0', _2VarSubA0_data), + ('2VarSubA1', _2VarSubA1_data), + ('3VarAddA0', _3VarAddA0_data), + ('3VarAddA1', _3VarAddA1_data), + ('3VarAddA2', _3VarAddA2_data), + ]} + +cmap_families = { + '2VarAddA': MultivarColormap([cmaps[f'2VarAddA{i}'] for i in range(2)], + 'sRGB_add', name='2VarAddA'), + '2VarSubA': MultivarColormap([cmaps[f'2VarSubA{i}'] for i in range(2)], + 'sRGB_sub', name='2VarSubA'), + '3VarAddA': MultivarColormap([cmaps[f'3VarAddA{i}'] for i in range(3)], + 'sRGB_add', name='3VarAddA'), +} diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index b960f363e9d4..5623e12a3c41 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -140,6 +140,13 @@ def do_constrained_layout(fig, h_pad, w_pad, w_pad=w_pad, hspace=hspace, wspace=wspace) else: _api.warn_external(warn_collapsed) + + if ((suptitle := fig._suptitle) is not None and + suptitle.get_in_layout() and suptitle._autopos): + x, _ = suptitle.get_position() + suptitle.set_position( + (x, layoutgrids[fig].get_inner_bbox().y1 + h_pad)) + suptitle.set_verticalalignment('bottom') else: _api.warn_external(warn_collapsed) reset_margins(layoutgrids, fig) @@ -627,7 +634,7 @@ def get_pos_and_bbox(ax, renderer): bbox : `~matplotlib.transforms.Bbox` Tight bounding box in figure coordinates. """ - fig = ax.figure + fig = ax.get_figure(root=False) pos = ax.get_position(original=True) # pos is in panel co-ords, but we need in figure for the layout pos = pos.transformed(fig.transSubfigure - fig.transFigure) @@ -699,7 +706,7 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): parents = cbax._colorbar_info['parents'] gs = parents[0].get_gridspec() - fig = cbax.figure + fig = cbax.get_figure(root=False) trans_fig_to_subfig = fig.transFigure - fig.transSubfigure cb_rspans, cb_cspans = get_cb_parent_spans(cbax) diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py index f44d7b2c7674..8cc7d623efe5 100644 --- a/lib/matplotlib/_docstring.py +++ b/lib/matplotlib/_docstring.py @@ -68,12 +68,6 @@ def __call__(self, func): func.__doc__ = inspect.cleandoc(func.__doc__) % self.params return func - def update(self, *args, **kwargs): - """ - Update ``self.params`` (which must be a dict) with the supplied args. - """ - self.params.update(*args, **kwargs) - class _ArtistKwdocLoader(dict): def __missing__(self, key): @@ -82,30 +76,52 @@ def __missing__(self, key): name = key[:-len(":kwdoc")] from matplotlib.artist import Artist, kwdoc try: - cls, = [cls for cls in _api.recursive_subclasses(Artist) - if cls.__name__ == name] + cls, = (cls for cls in _api.recursive_subclasses(Artist) + if cls.__name__ == name) except ValueError as e: raise KeyError(key) from e return self.setdefault(key, kwdoc(cls)) -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: """ - A `.Substitution` with two additional features: - - - Substitutions of the form ``%(classname:kwdoc)s`` (ending with the - literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the - given *classname*, and are substituted with the `.kwdoc` of that class. - - Decorating a class triggers substitution both on the class docstring and - on the class' ``__init__`` docstring (which is a commonly required - pattern for Artist subclasses). + A class to substitute formatted placeholders in docstrings. + + This is realized in a single instance ``_docstring.interpd``. + + Use `~._ArtistPropertiesSubstition.register` to define placeholders and + their substitution, e.g. ``_docstring.interpd.register(name="some value")``. + + Use this as a decorator to apply the substitution:: + + @_docstring.interpd + def some_func(): + '''Replace %(name)s.''' + + Decorating a class triggers substitution both on the class docstring and + on the class' ``__init__`` docstring (which is a commonly required + pattern for Artist subclasses). + + Substitutions of the form ``%(classname:kwdoc)s`` (ending with the + literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the + given *classname*, and are substituted with the `.kwdoc` of that class. """ def __init__(self): self.params = _ArtistKwdocLoader() + def register(self, **kwargs): + """ + Register substitutions. + + ``_docstring.interpd.register(name="some value")`` makes "name" available + as a named parameter that will be replaced by "some value". + """ + self.params.update(**kwargs) + def __call__(self, obj): - super().__call__(obj) + if obj.__doc__: + obj.__doc__ = inspect.cleandoc(obj.__doc__) % self.params if isinstance(obj, type) and obj.__init__ != object.__init__: self(obj.__init__) return obj @@ -122,4 +138,4 @@ def do_copy(target): # Create a decorator that will house the various docstring snippets reused # throughout Matplotlib. -dedent_interpd = interpd = _ArtistPropertiesSubstitution() +interpd = _ArtistPropertiesSubstitution() diff --git a/lib/matplotlib/_docstring.pyi b/lib/matplotlib/_docstring.pyi index bcb4b29ab922..fb52d0846123 100644 --- a/lib/matplotlib/_docstring.pyi +++ b/lib/matplotlib/_docstring.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable, TypeVar, overload +from collections.abc import Callable +from typing import Any, TypeVar, overload _T = TypeVar('_T') @@ -20,8 +21,9 @@ class _ArtistKwdocLoader(dict[str, str]): def __missing__(self, key: str) -> str: ... -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: def __init__(self) -> None: ... + def register(self, **kwargs) -> None: ... def __call__(self, obj: _T) -> _T: ... diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index c8c50f7c3028..773011d36bf6 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -181,5 +181,7 @@ def demo(): + ", ".join([f"'{cs.name}'" for cs in CapStyle]) \ + "}" -_docstring.interpd.update({'JoinStyle': JoinStyle.input_description, - 'CapStyle': CapStyle.input_description}) +_docstring.interpd.register( + JoinStyle=JoinStyle.input_description, + CapStyle=CapStyle.input_description, +) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 6e4df209b1f9..6a1d9add9e8a 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -29,7 +29,7 @@ from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import FT2Font, FT2Image, KERNING_DEFAULT +from .ft2font import FT2Font, FT2Image, Kerning, LoadFlags from packaging.version import parse as parse_version from pyparsing import __version__ as pyparsing_version @@ -153,7 +153,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: w = xmax - xmin h = ymax - ymin - self.box.depth d = ymax - ymin - self.box.height - image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) + image = FT2Image(int(np.ceil(w)), int(np.ceil(h + max(d, 0)))) # Ideally, we could just use self.glyphs and self.rects here, shifting # their coordinates by (-xmin, -ymin), but this yields slightly @@ -163,7 +163,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: for ox, oy, info in shifted.glyphs: info.font.draw_glyph_to_bitmap( - image, ox, oy - info.metrics.iceberg, info.glyph, + image, int(ox), int(oy - info.metrics.iceberg), info.glyph, antialiased=antialiased) for x1, y1, x2, y2 in shifted.rects: height = max(int(y2 - y1) - 1, 0) @@ -172,7 +172,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: y = int(center - (height + 1) / 2) else: y = int(y1) - image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) + image.draw_rect_filled(int(x1), y, int(np.ceil(x2)), y + height) return RasterParse(0, 0, w, h + d, d, image) @@ -227,14 +227,14 @@ class Fonts(abc.ABC): to do the actual drawing. """ - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): """ Parameters ---------- default_font_prop : `~.font_manager.FontProperties` The default non-math font, or the base font for Unicode (generic) font rendering. - load_glyph_flags : int + load_glyph_flags : `.ft2font.LoadFlags` Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and ``FT_Load_Char`` for FreeType-based fonts). """ @@ -332,7 +332,7 @@ class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): (through FT2Font). """ - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): super().__init__(default_font_prop, load_glyph_flags) # Per-instance cache. self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] @@ -376,30 +376,30 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, font.set_size(fontsize, dpi) glyph = font.load_char(num, flags=self.load_glyph_flags) - xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] + xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) metrics = FontMetrics( - advance = glyph.linearHoriAdvance/65536.0, - height = glyph.height/64.0, - width = glyph.width/64.0, - xmin = xmin, - xmax = xmax, - ymin = ymin+offset, - ymax = ymax+offset, + advance=glyph.linearHoriAdvance / 65536, + height=glyph.height / 64, + width=glyph.width / 64, + xmin=xmin, + xmax=xmax, + ymin=ymin + offset, + ymax=ymax + offset, # iceberg is the equivalent of TeX's "height" - iceberg = glyph.horiBearingY/64.0 + offset, - slanted = slanted - ) + iceberg=glyph.horiBearingY / 64 + offset, + slanted=slanted + ) return FontInfo( - font = font, - fontsize = fontsize, - postscript_name = font.postscript_name, - metrics = metrics, - num = num, - glyph = glyph, - offset = offset - ) + font=font, + fontsize=fontsize, + postscript_name=font.postscript_name, + metrics=metrics, + num=num, + glyph=glyph, + offset=offset + ) def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: font = self._get_font(fontname) @@ -426,7 +426,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64 + return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) @@ -448,7 +448,7 @@ class BakomaFonts(TruetypeFonts): 'ex': 'cmex10', } - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags) super().__init__(default_font_prop, load_glyph_flags) @@ -557,7 +557,7 @@ class UnicodeFonts(TruetypeFonts): 0x2212: 0x00A1, # Minus sign. } - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly fallback_rc = mpl.rcParams['mathtext.fallback'] font_cls: type[TruetypeFonts] | None = { @@ -672,7 +672,7 @@ def get_sized_alternatives_for_symbol(self, fontname: str, class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): _fontmap: dict[str | int, str] = {} - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly if isinstance(self, DejaVuSerifFonts): self._fallback_font = StixFonts(default_font_prop, load_glyph_flags) @@ -776,7 +776,7 @@ class StixFonts(UnicodeFonts): _fallback_font = None _sans = False - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) for key, name in self._fontmap.items(): fullpath = findfont(name) @@ -2278,14 +2278,14 @@ def symbol(self, s: str, loc: int, if c in self._spaced_symbols: # iterate until we find previous character, needed for cases - # such as ${ -2}$, $ -2$, or $ -2$. + # such as $=-2$, ${ -2}$, $ -2$, or $ -2$. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') # Binary operators at start of string should not be spaced # Also, operators in sub- or superscripts should not be spaced if (self._in_subscript_or_superscript or ( c in self._binary_operators and ( - len(s[:loc].split()) == 0 or prev_char == '{' or - prev_char in self._left_delims))): + len(s[:loc].split()) == 0 or prev_char in { + '{', *self._left_delims, *self._relation_symbols}))): return [char] else: return [Hlist([self._make_space(0.2), @@ -2645,7 +2645,7 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty if rdelim == '': rdelim = '.' return self._auto_sized_delimiter(ldelim, - T.cast(list[T.Union[Box, Char, str]], + T.cast(list[Box | Char | str], result), rdelim) return result @@ -2786,7 +2786,7 @@ def _auto_sized_delimiter(self, front: str, del middle[idx] # There should only be \middle and its delimiter as str, which have # just been removed. - middle_part = T.cast(list[T.Union[Box, Char]], middle) + middle_part = T.cast(list[Box | Char], middle) else: height = 0 depth = 0 diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 8928800a108b..5819ee743044 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from typing import overload, Union +from typing import overload latex_to_bakoma = { '\\__sqrt__' : ('cmex10', 0x70), @@ -1113,11 +1113,10 @@ # Each element is a 4-tuple of the form: # src_start, src_end, dst_font, dst_start -_EntryTypeIn = tuple[str, str, str, Union[str, int]] +_EntryTypeIn = tuple[str, str, str, str | int] _EntryTypeOut = tuple[int, int, str, int] -_stix_virtual_fonts: dict[str, Union[dict[ - str, list[_EntryTypeIn]], list[_EntryTypeIn]]] = { +_stix_virtual_fonts: dict[str, dict[str, list[_EntryTypeIn]] | list[_EntryTypeIn]] = { 'bb': { "rm": [ ("\N{DIGIT ZERO}", @@ -1729,8 +1728,7 @@ def _normalize_stix_fontcodes(d): return {k: _normalize_stix_fontcodes(v) for k, v in d.items()} -stix_virtual_fonts: dict[str, Union[dict[str, list[_EntryTypeOut]], - list[_EntryTypeOut]]] +stix_virtual_fonts: dict[str, dict[str, list[_EntryTypeOut]] | list[_EntryTypeOut]] stix_virtual_fonts = _normalize_stix_fontcodes(_stix_virtual_fonts) # Free redundant list now that it has been normalized diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index cb6ca41d02c9..a3861aef5920 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -108,7 +108,7 @@ def _set_new_active_manager(cls, manager): manager._cidgcf = manager.canvas.mpl_connect( "button_press_event", lambda event: cls.set_active(manager)) fig = manager.canvas.figure - fig.number = manager.num + fig._number = manager.num label = fig.get_label() if label: manager.set_window_title(label) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index dc0540ea14e4..b9603b114bc2 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -7,7 +7,7 @@ import dataclasses from . import _api -from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING, FT2Font +from .ft2font import FT2Font, Kerning, LoadFlags @dataclasses.dataclass(frozen=True) @@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames): f"Matplotlib currently does not support {block} natively.") -def layout(string, font, *, kern_mode=KERNING_DEFAULT): +def layout(string, font, *, kern_mode=Kerning.DEFAULT): """ Render *string* with *font*. @@ -56,7 +56,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT): The string to be rendered. font : FT2Font The font. - kern_mode : int + kern_mode : Kerning A FreeType kerning mode. Yields @@ -76,7 +76,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT): if prev_glyph_idx is not None else 0. ) x += kern - glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING) + glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING) yield LayoutItem(font, char, glyph_idx, x, kern) x += glyph.linearHoriAdvance / 65536 prev_glyph_idx = glyph_idx diff --git a/lib/matplotlib/_tri.pyi b/lib/matplotlib/_tri.pyi index b6e79d7140b3..a0c710fc2309 100644 --- a/lib/matplotlib/_tri.pyi +++ b/lib/matplotlib/_tri.pyi @@ -1,26 +1,36 @@ # This is a private module implemented in C++ -# As such these type stubs are overly generic, but here to allow these types -# as return types for public methods -from typing import Any, final +from typing import final + +import numpy as np +import numpy.typing as npt @final class TrapezoidMapTriFinder: - def __init__(self, *args, **kwargs) -> None: ... - def find_many(self, *args, **kwargs) -> Any: ... - def get_tree_stats(self, *args, **kwargs) -> Any: ... - def initialize(self, *args, **kwargs) -> Any: ... - def print_tree(self, *args, **kwargs) -> Any: ... + def __init__(self, triangulation: Triangulation): ... + def find_many(self, x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> npt.NDArray[np.int_]: ... + def get_tree_stats(self) -> list[int | float]: ... + def initialize(self) -> None: ... + def print_tree(self) -> None: ... @final class TriContourGenerator: - def __init__(self, *args, **kwargs) -> None: ... - def create_contour(self, *args, **kwargs) -> Any: ... - def create_filled_contour(self, *args, **kwargs) -> Any: ... + def __init__(self, triangulation: Triangulation, z: npt.NDArray[np.float64]): ... + def create_contour(self, level: float) -> tuple[list[float], list[int]]: ... + def create_filled_contour(self, lower_level: float, upper_level: float) -> tuple[list[float], list[int]]: ... @final class Triangulation: - def __init__(self, *args, **kwargs) -> None: ... - def calculate_plane_coefficients(self, *args, **kwargs) -> Any: ... - def get_edges(self, *args, **kwargs) -> Any: ... - def get_neighbors(self, *args, **kwargs) -> Any: ... - def set_mask(self, *args, **kwargs) -> Any: ... + def __init__( + self, + x: npt.NDArray[np.float64], + y: npt.NDArray[np.float64], + triangles: npt.NDArray[np.int_], + mask: npt.NDArray[np.bool_] | tuple[()], + edges: npt.NDArray[np.int_] | tuple[()], + neighbors: npt.NDArray[np.int_] | tuple[()], + correct_triangle_orientation: bool, + ): ... + def calculate_plane_coefficients(self, z: npt.ArrayLike) -> npt.NDArray[np.float64]: ... + def get_edges(self) -> npt.NDArray[np.int_]: ... + def get_neighbors(self) -> npt.NDArray[np.int_]: ... + def set_mask(self, mask: npt.NDArray[np.bool_] | tuple[()]) -> None: ... diff --git a/lib/matplotlib/_ttconv.pyi b/lib/matplotlib/_ttconv.pyi deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index b402c5fdb4da..2be61284073a 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -28,13 +28,6 @@ subprocess_creation_flags = ( subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0) -# Other potential writing methods: -# * http://pymedia.org/ -# * libming (produces swf) python wrappers: https://github.com/libming/libming -# * Wrap x264 API: - -# (https://stackoverflow.com/q/2940671/) - def adjusted_figsize(w, h, dpi, n): """ @@ -185,6 +178,14 @@ def frame_size(self): w, h = self.fig.get_size_inches() return int(w * self.dpi), int(h * self.dpi) + def _supports_transparency(self): + """ + Whether this writer supports transparency. + + Writers may consult output file type and codec to determine this at runtime. + """ + return False + @abc.abstractmethod def grab_frame(self, **savefig_kwargs): """ @@ -475,6 +476,9 @@ def finish(self): @writers.register('pillow') class PillowWriter(AbstractMovieWriter): + def _supports_transparency(self): + return True + @classmethod def isAvailable(cls): return True @@ -488,8 +492,15 @@ def grab_frame(self, **savefig_kwargs): buf = BytesIO() self.fig.savefig( buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi}) - self._frames.append(Image.frombuffer( - "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1)) + im = Image.frombuffer( + "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1) + if im.getextrema()[3][0] < 255: + # This frame has transparency, so we'll just add it as is. + self._frame.append(im) + else: + # Without transparency, we switch to RGB mode, which converts to P mode a + # little better if needed (specifically, this helps with GIF output.) + self._frames.append(im.convert("RGB")) def finish(self): self._frames[0].save( @@ -510,11 +521,26 @@ class FFMpegBase: _exec_key = 'animation.ffmpeg_path' _args_key = 'animation.ffmpeg_args' + def _supports_transparency(self): + suffix = Path(self.outfile).suffix + if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}: + return True + # This list was found by going through `ffmpeg -codecs` for video encoders, + # running them with _support_transparency() forced to True, and checking that + # the "Pixel format" in Kdenlive included alpha. Note this is not a guarantee + # that transparency will work; you may also need to pass `-pix_fmt`, but we + # trust the user has done so if they are asking for these formats. + return self.codec in { + 'apng', 'avrp', 'bmp', 'cfhd', 'dpx', 'ffv1', 'ffvhuff', 'gif', 'huffyuv', + 'jpeg2000', 'ljpeg', 'png', 'prores', 'prores_aw', 'prores_ks', 'qtrle', + 'rawvideo', 'targa', 'tiff', 'utvideo', 'v408', } + @property def output_args(self): args = [] - if Path(self.outfile).suffix == '.gif': - self.codec = 'gif' + suffix = Path(self.outfile).suffix + if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}: + self.codec = suffix[1:] else: args.extend(['-vcodec', self.codec]) extra_args = (self.extra_args if self.extra_args is not None @@ -525,11 +551,17 @@ def output_args(self): # macOS). Also fixes internet explorer. This is as of 2015/10/29. if self.codec == 'h264' and '-pix_fmt' not in extra_args: args.extend(['-pix_fmt', 'yuv420p']) - # For GIF, we're telling FFMPEG to split the video stream, to generate + # For GIF, we're telling FFmpeg to split the video stream, to generate # a palette, and then use it for encoding. elif self.codec == 'gif' and '-filter_complex' not in extra_args: args.extend(['-filter_complex', 'split [a][b];[a] palettegen [p];[b][p] paletteuse']) + # For AVIF, we're telling FFmpeg to split the video stream, extract the alpha, + # in order to place it in a secondary stream, as needed by AVIF-in-FFmpeg. + elif self.codec == 'avif' and '-filter_complex' not in extra_args: + args.extend(['-filter_complex', + 'split [rgb][rgba]; [rgba] alphaextract [alpha]', + '-map', '[rgb]', '-map', '[alpha]']) if self.bitrate > 0: args.extend(['-b', '%dk' % self.bitrate]) # %dk: bitrate in kbps. for k, v in self.metadata.items(): @@ -617,6 +649,10 @@ class ImageMagickBase: _exec_key = 'animation.convert_path' _args_key = 'animation.convert_args' + def _supports_transparency(self): + suffix = Path(self.outfile).suffix + return suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'} + def _args(self): # ImageMagick does not recognize "raw". fmt = "rgba" if self.frame_format == "raw" else self.frame_format @@ -1052,22 +1088,23 @@ def func(current_frame: int, total_frames: int) -> Any # since GUI widgets are gone. Either need to remove extra code to # allow for this non-existent use case or find a way to make it work. - facecolor = savefig_kwargs.get('facecolor', - mpl.rcParams['savefig.facecolor']) - if facecolor == 'auto': - facecolor = self._fig.get_facecolor() - def _pre_composite_to_white(color): r, g, b, a = mcolors.to_rgba(color) return a * np.array([r, g, b]) + 1 - a - savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor) - savefig_kwargs['transparent'] = False # just to be safe! # canvas._is_saving = True makes the draw_event animation-starting # callback a no-op; canvas.manager = None prevents resizing the GUI # widget (both are likewise done in savefig()). - with writer.saving(self._fig, filename, dpi), \ - cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None): + with (writer.saving(self._fig, filename, dpi), + cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)): + if not writer._supports_transparency(): + facecolor = savefig_kwargs.get('facecolor', + mpl.rcParams['savefig.facecolor']) + if facecolor == 'auto': + facecolor = self._fig.get_facecolor() + savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor) + savefig_kwargs['transparent'] = False # just to be safe! + for anim in all_anim: anim._init_draw() # Clear the initial frame frame_number = 0 diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 735c2eb59cf5..17724c8b027a 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1,10 +1,11 @@ from collections import namedtuple import contextlib -from functools import cache, wraps +from functools import cache, reduce, wraps import inspect from inspect import Signature, Parameter import logging from numbers import Number, Real +import operator import re import warnings @@ -12,8 +13,6 @@ import matplotlib as mpl from . import _api, cbook -from .colors import BoundaryNorm -from .cm import ScalarMappable from .path import Path from .transforms import (BboxBase, Bbox, IdentityTransform, Transform, TransformedBbox, TransformedPatchPath, TransformedPath) @@ -75,8 +74,8 @@ def draw_wrapper(artist, renderer): renderer.stop_filter(artist.get_agg_filter()) if artist.get_rasterized(): renderer._raster_depth -= 1 - if (renderer._rasterizing and artist.figure and - artist.figure.suppressComposite): + if (renderer._rasterizing and (fig := artist.get_figure(root=True)) and + fig.suppressComposite): # restart rasterizing to prevent merging renderer.stop_rasterizing() renderer.start_rasterizing() @@ -181,7 +180,7 @@ def __init__(self): self._stale = True self.stale_callback = None self._axes = None - self.figure = None + self._parent_figure = None self._transform = None self._transformSet = False @@ -248,10 +247,10 @@ def remove(self): self.axes = None # decouple the artist from the Axes _ax_flag = True - if self.figure: + if (fig := self.get_figure(root=False)) is not None: if not _ax_flag: - self.figure.stale = True - self.figure = None + fig.stale = True + self._parent_figure = None else: raise NotImplementedError('cannot remove artist') @@ -473,8 +472,9 @@ def _different_canvas(self, event): return False, {} # subclass-specific implementation follows """ - return (getattr(event, "canvas", None) is not None and self.figure is not None - and event.canvas is not self.figure.canvas) + return (getattr(event, "canvas", None) is not None + and (fig := self.get_figure(root=True)) is not None + and event.canvas is not fig.canvas) def contains(self, mouseevent): """ @@ -504,7 +504,7 @@ def pickable(self): -------- .Artist.set_picker, .Artist.get_picker, .Artist.pick """ - return self.figure is not None and self._picker is not None + return self.get_figure(root=False) is not None and self._picker is not None def pick(self, mouseevent): """ @@ -526,7 +526,7 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - PickEvent("pick_event", self.figure.canvas, + PickEvent("pick_event", self.get_figure(root=True).canvas, mouseevent, self, **prop)._process() # Pick children @@ -720,34 +720,49 @@ def set_path_effects(self, path_effects): def get_path_effects(self): return self._path_effects - def get_figure(self): - """Return the `.Figure` instance the artist belongs to.""" - return self.figure + def get_figure(self, root=False): + """ + Return the `.Figure` or `.SubFigure` instance the artist belongs to. + + Parameters + ---------- + root : bool, default=False + If False, return the (Sub)Figure this artist is on. If True, + return the root Figure for a nested tree of SubFigures. + """ + if root and self._parent_figure is not None: + return self._parent_figure.get_figure(root=True) + + return self._parent_figure def set_figure(self, fig): """ - Set the `.Figure` instance the artist belongs to. + Set the `.Figure` or `.SubFigure` instance the artist belongs to. Parameters ---------- - fig : `~matplotlib.figure.Figure` + fig : `~matplotlib.figure.Figure` or `~matplotlib.figure.SubFigure` """ # if this is a no-op just return - if self.figure is fig: + if self._parent_figure is fig: return # if we currently have a figure (the case of both `self.figure` # and *fig* being none is taken care of above) we then user is # trying to change the figure an artist is associated with which # is not allowed for the same reason as adding the same instance # to more than one Axes - if self.figure is not None: + if self._parent_figure is not None: raise RuntimeError("Can not put single artist in " "more than one figure") - self.figure = fig - if self.figure and self.figure is not self: + self._parent_figure = fig + if self._parent_figure and self._parent_figure is not self: self.pchanged() self.stale = True + figure = property(get_figure, set_figure, + doc=("The (Sub)Figure that the artist is on. For more " + "control, use the `get_figure` method.")) + def set_clip_box(self, clipbox): """ Set the artist's clip `.Bbox`. @@ -1175,7 +1190,8 @@ def _update_props(self, props, errfmt): Helper for `.Artist.set` and `.Artist.update`. *errfmt* is used to generate error messages for invalid property - names; it gets formatted with ``type(self)`` and the property name. + names; it gets formatted with ``type(self)`` for "{cls}" and the + property name for "{prop_name}". """ ret = [] with cbook._setattr_cm(self, eventson=False): @@ -1188,7 +1204,8 @@ def _update_props(self, props, errfmt): func = getattr(self, f"set_{k}", None) if not callable(func): raise AttributeError( - errfmt.format(cls=type(self), prop_name=k)) + errfmt.format(cls=type(self), prop_name=k), + name=k) ret.append(func(v)) if ret: self.pchanged() @@ -1273,7 +1290,8 @@ def matchfunc(x): raise ValueError('match must be None, a matplotlib.artist.Artist ' 'subclass, or a callable') - artists = sum([c.findobj(matchfunc) for c in self.get_children()], []) + artists = reduce(operator.iadd, + [c.findobj(matchfunc) for c in self.get_children()], []) if include_self and matchfunc(self): artists.append(self) return artists @@ -1327,37 +1345,11 @@ def format_cursor_data(self, data): -------- get_cursor_data """ - if np.ndim(data) == 0 and isinstance(self, ScalarMappable): - # This block logically belongs to ScalarMappable, but can't be - # implemented in it because most ScalarMappable subclasses inherit - # from Artist first and from ScalarMappable second, so - # Artist.format_cursor_data would always have precedence over - # ScalarMappable.format_cursor_data. - n = self.cmap.N - if np.ma.getmask(data): - return "[]" - normed = self.norm(data) - if np.isfinite(normed): - if isinstance(self.norm, BoundaryNorm): - # not an invertible normalization mapping - cur_idx = np.argmin(np.abs(self.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: - # singular norms, use delta of 10% of only value - delta = np.abs(self.norm.vmin * .1) - else: - # Midpoints of neighboring color intervals. - neighbors = self.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}]" + if np.ndim(data) == 0 and hasattr(self, "_format_cursor_data_override"): + # workaround for ScalarMappable to be able to define its own + # format_cursor_data(). See ScalarMappable._format_cursor_data_override + # for details. + return self._format_cursor_data_override(data) else: try: data[0] @@ -1594,7 +1586,8 @@ def aliased_name_rest(self, s, target): if target in self._NOT_LINKABLE: return f'``{s}``' - aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) + aliases = ''.join( + f' or :meth:`{a} <{target}>`' for a in sorted(self.aliasd.get(s, []))) return f':meth:`{s} <{target}>`{aliases}' def pprint_setters(self, prop=None, leadingspace=2): diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 101e97a9a072..be23f69d44a6 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -15,9 +15,11 @@ from .transforms import ( import numpy as np from collections.abc import Callable, Iterable -from typing import Any, NamedTuple, TextIO, overload +from typing import Any, Literal, NamedTuple, TextIO, overload, TypeVar from numpy.typing import ArrayLike +_T_Artist = TypeVar("_T_Artist", bound=Artist) + def allow_rasterization(draw): ... class _XYPair(NamedTuple): @@ -29,7 +31,8 @@ class _Unset: ... class Artist: zorder: float stale_callback: Callable[[Artist, bool], None] | None - figure: Figure | SubFigure | None + @property + def figure(self) -> Figure | SubFigure: ... clipbox: BboxBase | None def __init__(self) -> None: ... def remove(self) -> None: ... @@ -85,8 +88,13 @@ class Artist: ) -> None: ... def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ... def get_path_effects(self) -> list[AbstractPathEffect]: ... - def get_figure(self) -> Figure | None: ... - def set_figure(self, fig: Figure) -> None: ... + @overload + def get_figure(self, root: Literal[True]) -> Figure | None: ... + @overload + def get_figure(self, root: Literal[False]) -> Figure | SubFigure | None: ... + @overload + def get_figure(self, root: bool = ...) -> Figure | SubFigure | None: ... + def set_figure(self, fig: Figure | SubFigure) -> None: ... def set_clip_box(self, clipbox: BboxBase | None) -> None: ... def set_clip_path( self, @@ -128,11 +136,21 @@ class Artist: def update(self, props: dict[str, Any]) -> list[Any]: ... def _internal_update(self, kwargs: Any) -> list[Any]: ... def set(self, **kwargs: Any) -> list[Any]: ... + + @overload def findobj( self, - match: None | Callable[[Artist], bool] | type[Artist] = ..., + match: None | Callable[[Artist], bool] = ..., include_self: bool = ..., ) -> list[Artist]: ... + + @overload + def findobj( + self, + match: type[_T_Artist], + include_self: bool = ..., + ) -> list[_T_Artist]: ... + def get_cursor_data(self, event: MouseEvent) -> Any: ... def format_cursor_data(self, data: Any) -> str: ... def get_mouseover(self) -> bool: ... diff --git a/lib/matplotlib/axes/__init__.py b/lib/matplotlib/axes/__init__.py index 9f2913957194..cdc31f17aae6 100644 --- a/lib/matplotlib/axes/__init__.py +++ b/lib/matplotlib/axes/__init__.py @@ -1,5 +1,5 @@ from . import _base -from ._axes import Axes # noqa: F401 +from ._axes import Axes # Backcompat. Subplot = Axes diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 040c5a4ba4e9..679499a4eab3 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6,16 +6,17 @@ import re import numpy as np -from numpy import ma import matplotlib as mpl import matplotlib.category # Register category unit converter as side effect. import matplotlib.cbook as cbook import matplotlib.collections as mcoll +import matplotlib.colorizer as mcolorizer import matplotlib.colors as mcolors import matplotlib.contour as mcontour -import matplotlib.dates # noqa # Register date unit converter as side effect. +import matplotlib.dates # noqa: F401, Register date unit converter as side effect. import matplotlib.image as mimage +import matplotlib.inset as minset import matplotlib.legend as mlegend import matplotlib.lines as mlines import matplotlib.markers as mmarkers @@ -36,6 +37,7 @@ _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.transforms import _ScaledRotation _log = logging.getLogger(__name__) @@ -85,13 +87,6 @@ class Axes(_AxesBase): methods instead; e.g. from `.pyplot` or `.Figure`: `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. - Attributes - ---------- - dataLim : `.Bbox` - The bounding box enclosing all data displayed in the Axes. - viewLim : `.Bbox` - The view limits in data coordinates. - """ ### Labelling, legend and texts @@ -220,7 +215,7 @@ def get_legend_handles_labels(self, legend_handler_map=None): [self], legend_handler_map) return handles, labels - @_docstring.dedent_interpd + @_docstring.interpd def legend(self, *args, **kwargs): """ Place a legend on the Axes. @@ -406,8 +401,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): # This puts the rectangle into figure-relative coordinates. inset_locator = _TransformedBoundsLocator(bounds, transform) bounds = inset_locator(self, None).bounds - projection_class, pkw = self.figure._process_projection_requirements(**kwargs) - inset_ax = projection_class(self.figure, bounds, zorder=zorder, **pkw) + fig = self.get_figure(root=False) + projection_class, pkw = fig._process_projection_requirements(**kwargs) + inset_ax = projection_class(fig, bounds, zorder=zorder, **pkw) # this locator lets the axes move if in data coordinates. # it gets called in `ax.apply_aspect() (of all places) @@ -417,10 +413,10 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax - @_docstring.dedent_interpd - def indicate_inset(self, bounds, inset_ax=None, *, transform=None, + @_docstring.interpd + def indicate_inset(self, bounds=None, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, - zorder=4.99, **kwargs): + zorder=None, **kwargs): """ Add an inset indicator to the Axes. This is a rectangle on the plot at the position indicated by *bounds* that optionally has lines that @@ -432,18 +428,19 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Parameters ---------- - bounds : [x0, y0, width, height] + bounds : [x0, y0, width, height], optional Lower-left corner of rectangle to be marked, and its width - and height. + and height. If not set, the bounds will be calculated from the + data limits of *inset_ax*, which must be supplied. - inset_ax : `.Axes` + inset_ax : `.Axes`, optional An optional inset Axes to draw connecting lines to. Two lines are drawn connecting the indicator box to the inset Axes on corners chosen so as to not overlap with the indicator box. transform : `.Transform` Transform for the rectangle coordinates. Defaults to - `ax.transAxes`, i.e. the units of *rect* are in Axes-relative + ``ax.transAxes``, i.e. the units of *rect* are in Axes-relative coordinates. facecolor : :mpltype:`color`, default: 'none' @@ -452,8 +449,10 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, edgecolor : :mpltype:`color`, default: '0.5' Color of the rectangle and color of the connecting lines. - alpha : float, default: 0.5 - Transparency of the rectangle and connector lines. + alpha : float or None, default: 0.5 + Transparency of the rectangle and connector lines. If not + ``None``, this overrides any alpha value included in the + *facecolor* and *edgecolor* parameters. zorder : float, default: 4.99 Drawing order of the rectangle and connector lines. The default, @@ -466,15 +465,20 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Returns ------- - rectangle_patch : `.patches.Rectangle` - The indicator frame. + inset_indicator : `.inset.InsetIndicator` + An artist which contains + + inset_indicator.rectangle : `.Rectangle` + The indicator frame. - connector_lines : 4-tuple of `.patches.ConnectionPatch` - The four connector lines connecting to (lower_left, upper_left, - lower_right upper_right) corners of *inset_ax*. Two lines are - set with visibility to *False*, but the user can set the - visibility to True if the automatic choice is not deemed correct. + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ # to make the Axes connectors work, we need to apply the aspect to # the parent Axes. @@ -484,51 +488,13 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, transform = self.transData kwargs.setdefault('label', '_indicate_inset') - x, y, width, height = bounds - rectangle_patch = mpatches.Rectangle( - (x, y), width, height, + indicator_patch = minset.InsetIndicator( + bounds, inset_ax=inset_ax, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, zorder=zorder, transform=transform, **kwargs) - self.add_patch(rectangle_patch) - - connects = [] - - if inset_ax is not None: - # connect the inset_axes to the rectangle - for xy_inset_ax in [(0, 0), (0, 1), (1, 0), (1, 1)]: - # inset_ax positions are in axes coordinates - # The 0, 1 values define the four edges if the inset_ax - # lower_left, upper_left, lower_right upper_right. - ex, ey = xy_inset_ax - if self.xaxis.get_inverted(): - ex = 1 - ex - if self.yaxis.get_inverted(): - ey = 1 - ey - xy_data = x + ex * width, y + ey * height - p = mpatches.ConnectionPatch( - xyA=xy_inset_ax, coordsA=inset_ax.transAxes, - xyB=xy_data, coordsB=self.transData, - arrowstyle="-", zorder=zorder, - edgecolor=edgecolor, alpha=alpha) - connects.append(p) - self.add_patch(p) - - # decide which two of the lines to keep visible.... - pos = inset_ax.get_position() - bboxins = pos.transformed(self.figure.transSubfigure) - rectbbox = mtransforms.Bbox.from_bounds( - *bounds - ).transformed(transform) - x0 = rectbbox.x0 < bboxins.x0 - x1 = rectbbox.x1 < bboxins.x1 - y0 = rectbbox.y0 < bboxins.y0 - y1 = rectbbox.y1 < bboxins.y1 - connects[0].set_visible(x0 ^ y0) - connects[1].set_visible(x0 == y1) - connects[2].set_visible(x1 == y0) - connects[3].set_visible(x1 ^ y1) - - return rectangle_patch, tuple(connects) if connects else None + self.add_artist(indicator_patch) + + return indicator_patch def indicate_inset_zoom(self, inset_ax, **kwargs): """ @@ -552,25 +518,26 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): Returns ------- - rectangle_patch : `.patches.Rectangle` - Rectangle artist. - - connector_lines : 4-tuple of `.patches.ConnectionPatch` - Each of four connector lines coming from the rectangle drawn on - this axis, in the order lower left, upper left, lower right, - upper right. - Two are set with visibility to *False*, but the user can - set the visibility to *True* if the automatic choice is not deemed - correct. + inset_indicator : `.inset.InsetIndicator` + An artist which contains + + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ - xlim = inset_ax.get_xlim() - ylim = inset_ax.get_ylim() - rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) - return self.indicate_inset(rect, inset_ax, **kwargs) + return self.indicate_inset(None, inset_ax, **kwargs) - @_docstring.dedent_interpd - def secondary_xaxis(self, location, *, functions=None, transform=None, **kwargs): + @_docstring.interpd + def secondary_xaxis(self, location, functions=None, *, transform=None, **kwargs): """ Add a second x-axis to this `~.axes.Axes`. @@ -623,8 +590,8 @@ def invert(x): self.add_child_axes(secondary_ax) return secondary_ax - @_docstring.dedent_interpd - def secondary_yaxis(self, location, *, functions=None, transform=None, **kwargs): + @_docstring.interpd + def secondary_yaxis(self, location, functions=None, *, transform=None, **kwargs): """ Add a second y-axis to this `~.axes.Axes`. @@ -667,7 +634,7 @@ def secondary_yaxis(self, location, *, functions=None, transform=None, **kwargs) self.add_child_axes(secondary_ax) return secondary_ax - @_docstring.dedent_interpd + @_docstring.interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to the Axes. @@ -746,7 +713,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): self._add_text(t) return t - @_docstring.dedent_interpd + @_docstring.interpd def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, arrowprops=None, annotation_clip=None, **kwargs): # Signature must match Annotation. This is verified in @@ -762,27 +729,41 @@ def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, annotate.__doc__ = mtext.Annotation.__init__.__doc__ #### Lines and spans - @_docstring.dedent_interpd + @_docstring.interpd def axhline(self, y=0, xmin=0, xmax=1, **kwargs): """ - Add a horizontal line across the Axes. + Add a horizontal line spanning the whole or fraction of the Axes. + + Note: If you want to set x-limits in data coordinates, use + `~.Axes.hlines` instead. Parameters ---------- y : float, default: 0 - y position in data coordinates of the horizontal line. + y position in :ref:`data coordinates `. xmin : float, default: 0 - Should be between 0 and 1, 0 being the far left of the plot, 1 the - far right of the plot. + The start x-position in :ref:`axes coordinates `. + Should be between 0 and 1, 0 being the far left of the plot, + 1 the far right of the plot. xmax : float, default: 1 - Should be between 0 and 1, 0 being the far left of the plot, 1 the - far right of the plot. + The end x-position in :ref:`axes coordinates `. + Should be between 0 and 1, 0 being the far left of the plot, + 1 the far right of the plot. Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(xmin, y)``, ``(xmax, y)``. + Its transform is set such that *x* is in + :ref:`axes coordinates ` and *y* is in + :ref:`data coordinates `. + + This is still a generic line and the horizontal character is only + realized through using identical *y* values for both points. Thus, + if you want to change the *y* value later, you have to provide two + values ``line.set_ydata([3, 3])``. Other Parameters ---------------- @@ -831,27 +812,41 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): self._request_autoscale_view("y") return l - @_docstring.dedent_interpd + @_docstring.interpd def axvline(self, x=0, ymin=0, ymax=1, **kwargs): """ - Add a vertical line across the Axes. + Add a vertical line spanning the whole or fraction of the Axes. + + Note: If you want to set y-limits in data coordinates, use + `~.Axes.vlines` instead. Parameters ---------- x : float, default: 0 - x position in data coordinates of the vertical line. + y position in :ref:`data coordinates `. ymin : float, default: 0 + The start y-position in :ref:`axes coordinates `. Should be between 0 and 1, 0 being the bottom of the plot, 1 the top of the plot. ymax : float, default: 1 + The end y-position in :ref:`axes coordinates `. Should be between 0 and 1, 0 being the bottom of the plot, 1 the top of the plot. Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(x, ymin)``, ``(x, ymax)``. + Its transform is set such that *x* is in + :ref:`data coordinates ` and *y* is in + :ref:`axes coordinates `. + + This is still a generic line and the vertical character is only + realized through using identical *x* values for both points. Thus, + if you want to change the *x* value later, you have to provide two + values ``line.set_xdata([3, 3])``. Other Parameters ---------------- @@ -908,7 +903,7 @@ def _check_no_units(vals, names): raise ValueError(f"{name} must be a single scalar value, " f"but got {val}") - @_docstring.dedent_interpd + @_docstring.interpd def axline(self, xy1, xy2=None, *, slope=None, **kwargs): """ Add an infinitely long straight line. @@ -982,7 +977,7 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs): self._request_autoscale_view() return line - @_docstring.dedent_interpd + @_docstring.interpd def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): """ Add a horizontal span (rectangle) across the Axes. @@ -1037,7 +1032,7 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): self._request_autoscale_view("y") return p - @_docstring.dedent_interpd + @_docstring.interpd def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): """ Add a vertical span (rectangle) across the Axes. @@ -1100,6 +1095,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): self._request_autoscale_view("x") return p + @_api.make_keyword_only("3.9", "label") @_preprocess_data(replace_names=["y", "xmin", "xmax", "colors"], label_namer="y") def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', @@ -1191,6 +1187,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', self._request_autoscale_view() return lines + @_api.make_keyword_only("3.9", "label") @_preprocess_data(replace_names=["x", "ymin", "ymax", "colors"], label_namer="x") def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', @@ -1282,10 +1279,11 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', self._request_autoscale_view() return lines + @_api.make_keyword_only("3.9", "orientation") @_preprocess_data(replace_names=["positions", "lineoffsets", "linelengths", "linewidths", "colors", "linestyles"]) - @_docstring.dedent_interpd + @_docstring.interpd def eventplot(self, positions, orientation='horizontal', lineoffsets=1, linelengths=1, linewidths=None, colors=None, alpha=None, linestyles='solid', **kwargs): @@ -1531,7 +1529,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, # Uses a custom implementation of data-kwarg handling in # _process_plot_var_args. - @_docstring.dedent_interpd + @_docstring.interpd def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): """ Plot y versus x as lines and/or markers. @@ -1787,7 +1785,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): @_api.deprecated("3.9", alternative="plot") @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, **kwargs): """ @@ -1867,7 +1865,7 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, return self.plot(x, y, fmt, **kwargs) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def loglog(self, *args, **kwargs): """ Make a plot with log scaling on both the x- and y-axis. @@ -1921,7 +1919,7 @@ def loglog(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in {*dx, *dy}}) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def semilogx(self, *args, **kwargs): """ Make a plot with log scaling on the x-axis. @@ -1968,7 +1966,7 @@ def semilogx(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in d}) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def semilogy(self, *args, **kwargs): """ Make a plot with log scaling on the y-axis. @@ -2088,6 +2086,7 @@ def acorr(self, x, **kwargs): """ return self.xcorr(x, x, **kwargs) + @_api.make_keyword_only("3.9", "normed") @_preprocess_data(replace_names=["x", "y"], label_namer="y") def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, usevlines=True, maxlags=10, **kwargs): @@ -2322,8 +2321,58 @@ def _convert_dx(dx, x0, xconv, convert): dx = convert(dx) return dx + def _parse_bar_color_args(self, kwargs): + """ + Helper function to process color-related arguments of `.Axes.bar`. + + Argument precedence for facecolors: + + - kwargs['facecolor'] + - kwargs['color'] + - 'Result of ``self._get_patches_for_fill.get_next_color`` + + Argument precedence for edgecolors: + + - kwargs['edgecolor'] + - None + + Parameters + ---------- + self : Axes + + kwargs : dict + Additional kwargs. If these keys exist, we pop and process them: + 'facecolor', 'edgecolor', 'color' + Note: The dict is modified by this function. + + + Returns + ------- + facecolor + The facecolor. One or more colors as (N, 4) rgba array. + edgecolor + The edgecolor. Not normalized; may be any valid color spec or None. + """ + color = kwargs.pop('color', None) + + facecolor = kwargs.pop('facecolor', color) + edgecolor = kwargs.pop('edgecolor', None) + + facecolor = (facecolor if facecolor is not None + else self._get_patches_for_fill.get_next_color()) + + try: + facecolor = mcolors.to_rgba_array(facecolor) + except ValueError as err: + raise ValueError( + "'facecolor' or 'color' argument must be a valid color or" + "sequence of colors." + ) from err + + return facecolor, edgecolor + @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", **kwargs): r""" @@ -2377,7 +2426,12 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", Other Parameters ---------------- color : :mpltype:`color` or list of :mpltype:`color`, optional + The colors of the bar faces. This is an alias for *facecolor*. + If both are given, *facecolor* takes precedence. + + facecolor : :mpltype:`color` or list of :mpltype:`color`, optional The colors of the bar faces. + If both *color* and *facecolor are given, *facecolor* takes precedence. edgecolor : :mpltype:`color` or list of :mpltype:`color`, optional The colors of the bar edges. @@ -2442,10 +2496,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", bar. See :doc:`/gallery/lines_bars_and_markers/bar_stacked`. """ kwargs = cbook.normalize_kwargs(kwargs, mpatches.Patch) - color = kwargs.pop('color', None) - if color is None: - color = self._get_patches_for_fill.get_next_color() - edgecolor = kwargs.pop('edgecolor', None) + facecolor, edgecolor = self._parse_bar_color_args(kwargs) + linewidth = kwargs.pop('linewidth', None) hatch = kwargs.pop('hatch', None) @@ -2541,9 +2593,9 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", linewidth = itertools.cycle(np.atleast_1d(linewidth)) hatch = itertools.cycle(np.atleast_1d(hatch)) - color = itertools.chain(itertools.cycle(mcolors.to_rgba_array(color)), - # Fallback if color == "none". - itertools.repeat('none')) + facecolor = itertools.chain(itertools.cycle(facecolor), + # Fallback if color == "none". + itertools.repeat('none')) if edgecolor is None: edgecolor = itertools.repeat(None) else: @@ -2577,7 +2629,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", bottom = y patches = [] - args = zip(left, bottom, width, height, color, edgecolor, linewidth, + args = zip(left, bottom, width, height, facecolor, edgecolor, linewidth, hatch, patch_labels) for l, b, w, h, c, e, lw, htch, lbl in args: r = mpatches.Rectangle( @@ -2635,7 +2687,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", return bar_container # @_preprocess_data() # let 'bar' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def barh(self, y, width, height=0.8, left=None, *, align="center", data=None, **kwargs): r""" @@ -2740,7 +2792,7 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", data : indexable object, optional If given, all parameters also accept a string ``s``, which is - interpreted as ``data[s]`` (unless this raises an exception). + interpreted as ``data[s]`` if ``s`` is a key in ``data``. **kwargs : `.Rectangle` properties @@ -2929,7 +2981,7 @@ def sign(x): return annotations @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def broken_barh(self, xranges, yrange, **kwargs): """ Plot a horizontal sequence of rectangles. @@ -3155,6 +3207,7 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, self.add_container(stem_container) return stem_container + @_api.make_keyword_only("3.9", "explode") @_preprocess_data(replace_names=["x", "explode", "labels", "colors"]) def pie(self, x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, @@ -3295,9 +3348,9 @@ def pie(self, x, explode=None, labels=None, colors=None, if explode is None: explode = [0] * len(x) if len(x) != len(labels): - raise ValueError("'label' must be of length 'x'") + raise ValueError(f"'labels' must be of length 'x', not {len(labels)}") if len(x) != len(explode): - raise ValueError("'explode' must be of length 'x'") + raise ValueError(f"'explode' must be of length 'x', not {len(explode)}") if colors is None: get_next_color = self._get_patches_for_fill.get_next_color else: @@ -3310,7 +3363,7 @@ def get_next_color(): _api.check_isinstance(Real, radius=radius, startangle=startangle) if radius <= 0: - raise ValueError(f'radius must be a positive number, not {radius}') + raise ValueError(f"'radius' must be a positive number, not {radius}") # Starting theta1 is the start fraction of the circle theta1 = startangle / 360 @@ -3434,9 +3487,10 @@ def _errorevery_to_mask(x, errorevery): everymask[errorevery] = True return everymask + @_api.make_keyword_only("3.9", "ecolor") @_preprocess_data(replace_names=["x", "y", "xerr", "yerr"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def errorbar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=None, barsabove=False, lolims=False, uplims=False, @@ -3784,13 +3838,14 @@ def apply_mask(arrays, mask): caplines[dep_axis].append(mlines.Line2D( x_masked, y_masked, marker=marker, **eb_cap_style)) if self.name == 'polar': + trans_shift = self.transShift for axis in caplines: for l in caplines[axis]: # Rotate caps to be perpendicular to the error bars for theta, r in zip(l.get_xdata(), l.get_ydata()): - rotation = mtransforms.Affine2D().rotate(theta) + rotation = _ScaledRotation(theta=theta, trans_shift=trans_shift) if axis == 'y': - rotation.rotate(-np.pi / 2) + rotation += mtransforms.Affine2D().rotate(np.pi / 2) ms = mmarkers.MarkerStyle(marker=marker, transform=rotation) self.add_line(mlines.Line2D([theta], [r], marker=ms, @@ -3810,11 +3865,13 @@ def apply_mask(arrays, mask): return errorbar_container # (l0, caplines, barcols) + @_api.make_keyword_only("3.9", "notch") @_preprocess_data() @_api.rename_parameter("3.9", "labels", "tick_labels") - def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, - positions=None, widths=None, patch_artist=None, - bootstrap=None, usermedians=None, conf_intervals=None, + def boxplot(self, x, notch=None, sym=None, vert=None, + orientation='vertical', whis=None, positions=None, + widths=None, patch_artist=None, bootstrap=None, + usermedians=None, conf_intervals=None, meanline=None, showmeans=None, showcaps=None, showbox=None, showfliers=None, boxprops=None, tick_labels=None, flierprops=None, medianprops=None, @@ -3870,9 +3927,23 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, the fliers. If `None`, then the fliers default to 'b+'. More control is provided by the *flierprops* parameter. - vert : bool, default: :rc:`boxplot.vertical` - If `True`, draws vertical boxes. - If `False`, draw horizontal boxes. + vert : bool, optional + .. deprecated:: 3.11 + Use *orientation* instead. + + This is a pending deprecation for 3.10, with full deprecation + in 3.11 and removal in 3.13. + If this is given during the deprecation period, it overrides + the *orientation* parameter. + + If True, plots the boxes vertically. + If False, plots the boxes horizontally. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + If 'horizontal', plots the boxes horizontally. + Otherwise, plots the boxes vertically. + + .. versionadded:: 3.10 whis : float or (float, float), default: 1.5 The position of the whiskers. @@ -4040,8 +4111,6 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, labels=tick_labels, autorange=autorange) if notch is None: notch = mpl.rcParams['boxplot.notch'] - if vert is None: - vert = mpl.rcParams['boxplot.vertical'] if patch_artist is None: patch_artist = mpl.rcParams['boxplot.patchartist'] if meanline is None: @@ -4141,12 +4210,14 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, meanline=meanline, showfliers=showfliers, capprops=capprops, whiskerprops=whiskerprops, manage_ticks=manage_ticks, zorder=zorder, - capwidths=capwidths, label=label) + capwidths=capwidths, label=label, + orientation=orientation) return artists - def bxp(self, bxpstats, positions=None, widths=None, vert=True, - patch_artist=False, shownotches=False, showmeans=False, - showcaps=True, showbox=True, showfliers=True, + @_api.make_keyword_only("3.9", "widths") + def bxp(self, bxpstats, positions=None, widths=None, vert=None, + orientation='vertical', patch_artist=False, shownotches=False, + showmeans=False, showcaps=True, showbox=True, showfliers=True, boxprops=None, whiskerprops=None, flierprops=None, medianprops=None, capprops=None, meanprops=None, meanline=False, manage_ticks=True, zorder=None, @@ -4176,62 +4247,76 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, Parameters ---------- bxpstats : list of dicts - A list of dictionaries containing stats for each boxplot. - Required keys are: + A list of dictionaries containing stats for each boxplot. + Required keys are: - - ``med``: Median (scalar). - - ``q1``, ``q3``: First & third quartiles (scalars). - - ``whislo``, ``whishi``: Lower & upper whisker positions (scalars). + - ``med``: Median (scalar). + - ``q1``, ``q3``: First & third quartiles (scalars). + - ``whislo``, ``whishi``: Lower & upper whisker positions (scalars). - Optional keys are: + Optional keys are: - - ``mean``: Mean (scalar). Needed if ``showmeans=True``. - - ``fliers``: Data beyond the whiskers (array-like). - Needed if ``showfliers=True``. - - ``cilo``, ``cihi``: Lower & upper confidence intervals - about the median. Needed if ``shownotches=True``. - - ``label``: Name of the dataset (str). If available, - this will be used a tick label for the boxplot + - ``mean``: Mean (scalar). Needed if ``showmeans=True``. + - ``fliers``: Data beyond the whiskers (array-like). + Needed if ``showfliers=True``. + - ``cilo``, ``cihi``: Lower & upper confidence intervals + about the median. Needed if ``shownotches=True``. + - ``label``: Name of the dataset (str). If available, + this will be used a tick label for the boxplot positions : array-like, default: [1, 2, ..., n] - The positions of the boxes. The ticks and limits - are automatically set to match the positions. + The positions of the boxes. The ticks and limits + are automatically set to match the positions. widths : float or array-like, default: None - The widths of the boxes. The default is - ``clip(0.15*(distance between extreme positions), 0.15, 0.5)``. + The widths of the boxes. The default is + ``clip(0.15*(distance between extreme positions), 0.15, 0.5)``. capwidths : float or array-like, default: None - Either a scalar or a vector and sets the width of each cap. - The default is ``0.5*(width of the box)``, see *widths*. + Either a scalar or a vector and sets the width of each cap. + The default is ``0.5*(width of the box)``, see *widths*. + + vert : bool, optional + .. deprecated:: 3.11 + Use *orientation* instead. + + This is a pending deprecation for 3.10, with full deprecation + in 3.11 and removal in 3.13. + If this is given during the deprecation period, it overrides + the *orientation* parameter. + + If True, plots the boxes vertically. + If False, plots the boxes horizontally. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + If 'horizontal', plots the boxes horizontally. + Otherwise, plots the boxes vertically. - vert : bool, default: True - If `True` (default), makes the boxes vertical. - If `False`, makes horizontal boxes. + .. versionadded:: 3.10 patch_artist : bool, default: False - If `False` produces boxes with the `.Line2D` artist. - If `True` produces boxes with the `~matplotlib.patches.Patch` artist. + If `False` produces boxes with the `.Line2D` artist. + If `True` produces boxes with the `~matplotlib.patches.Patch` artist. shownotches, showmeans, showcaps, showbox, showfliers : bool - Whether to draw the CI notches, the mean value (both default to - False), the caps, the box, and the fliers (all three default to - True). + Whether to draw the CI notches, the mean value (both default to + False), the caps, the box, and the fliers (all three default to + True). boxprops, whiskerprops, capprops, flierprops, medianprops, meanprops :\ dict, optional - Artist properties for the boxes, whiskers, caps, fliers, medians, and - means. + Artist properties for the boxes, whiskers, caps, fliers, medians, and + means. meanline : bool, default: False - If `True` (and *showmeans* is `True`), will try to render the mean - as a line spanning the full width of the box according to - *meanprops*. Not recommended if *shownotches* is also True. - Otherwise, means will be shown as points. + If `True` (and *showmeans* is `True`), will try to render the mean + as a line spanning the full width of the box according to + *meanprops*. Not recommended if *shownotches* is also True. + Otherwise, means will be shown as points. manage_ticks : bool, default: True - If True, the tick locations and labels will be adjusted to match the - boxplot positions. + If True, the tick locations and labels will be adjusted to match the + boxplot positions. label : str or list of str, optional Legend labels. Use a single string when all boxes have the same style and @@ -4248,22 +4333,22 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, .. versionadded:: 3.9 zorder : float, default: ``Line2D.zorder = 2`` - The zorder of the resulting boxplot. + The zorder of the resulting boxplot. Returns ------- dict - A dictionary mapping each component of the boxplot to a list - of the `.Line2D` instances created. That dictionary has the - following keys (assuming vertical boxplots): - - - ``boxes``: main bodies of the boxplot showing the quartiles, and - the median's confidence intervals if enabled. - - ``medians``: horizontal lines at the median of each box. - - ``whiskers``: vertical lines up to the last non-outlier data. - - ``caps``: horizontal lines at the ends of the whiskers. - - ``fliers``: points representing data beyond the whiskers (fliers). - - ``means``: points or lines representing the means. + A dictionary mapping each component of the boxplot to a list + of the `.Line2D` instances created. That dictionary has the + following keys (assuming vertical boxplots): + + - ``boxes``: main bodies of the boxplot showing the quartiles, and + the median's confidence intervals if enabled. + - ``medians``: horizontal lines at the median of each box. + - ``whiskers``: vertical lines up to the last non-outlier data. + - ``caps``: horizontal lines at the ends of the whiskers. + - ``fliers``: points representing data beyond the whiskers (fliers). + - ``means``: points or lines representing the means. See Also -------- @@ -4326,8 +4411,30 @@ def merge_kw_rc(subkey, explicit, zdelta=0, usemarker=True): if meanprops is None or removed_prop not in meanprops: mean_kw[removed_prop] = '' + # vert and orientation parameters are linked until vert's + # deprecation period expires. vert only takes precedence + # if set to False. + if vert is None: + vert = mpl.rcParams['boxplot.vertical'] + else: + _api.warn_deprecated( + "3.11", + name="vert: bool", + alternative="orientation: {'vertical', 'horizontal'}", + pending=True, + ) + if vert is False: + orientation = 'horizontal' + _api.check_in_list(['horizontal', 'vertical'], orientation=orientation) + + if not mpl.rcParams['boxplot.vertical']: + _api.warn_deprecated( + "3.10", + name='boxplot.vertical', obj_type="rcparam" + ) + # vertical or horizontal plot? - maybe_swap = slice(None) if vert else slice(None, None, -1) + maybe_swap = slice(None) if orientation == 'vertical' else slice(None, None, -1) def do_plot(xs, ys, **kwargs): return self.plot(*[xs, ys][maybe_swap], **kwargs)[0] @@ -4452,7 +4559,7 @@ def do_patch(xs, ys, **kwargs): artist.set_label(lbl) if manage_ticks: - axis_name = "x" if vert else "y" + axis_name = "x" if orientation == 'vertical' else "y" interval = getattr(self.dataLim, f"interval{axis_name}") axis = self._axis_map[axis_name] positions = axis.convert_units(positions) @@ -4636,6 +4743,7 @@ def invalid_shape_exception(csize, xsize): colors = None # use cmap, norm after collection is created return c, colors, edgecolors + @_api.make_keyword_only("3.9", "marker") @_preprocess_data(replace_names=["x", "y", "s", "linewidths", "edgecolors", "c", "facecolor", "facecolors", "color"], @@ -4643,7 +4751,7 @@ def invalid_shape_exception(csize, xsize): @_docstring.interpd def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, *, - edgecolors=None, plotnonfinite=False, **kwargs): + edgecolors=None, colorizer=None, plotnonfinite=False, **kwargs): """ A scatter plot of *y* vs. *x* with varying marker size and/or color. @@ -4730,6 +4838,10 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, is determined like with 'face', i.e. from *c*, *colors*, or *facecolors*. + %(colorizer_doc)s + + This parameter is ignored if *c* is RGB(A). + plotnonfinite : bool, default: False Whether to plot points with nonfinite *c* (i.e. ``inf``, ``-inf`` or ``nan``). If ``True`` the points are drawn with the *bad* @@ -4743,7 +4855,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, ---------------- data : indexable object, optional DATA_PARAMETER_PLACEHOLDER - **kwargs : `~matplotlib.collections.Collection` properties + **kwargs : `~matplotlib.collections.PathCollection` properties + %(PathCollection:kwdoc)s See Also -------- @@ -4884,9 +4997,14 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, ) collection.set_transform(mtransforms.IdentityTransform()) if colors is None: + if colorizer: + collection._set_colorizer_check_keywords(colorizer, cmap=cmap, + norm=norm, vmin=vmin, + vmax=vmax) + else: + collection.set_cmap(cmap) + collection.set_norm(norm) collection.set_array(c) - collection.set_cmap(cmap) - collection.set_norm(norm) collection._scale_norm(norm, vmin, vmax) else: extra_kwargs = { @@ -4916,14 +5034,15 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, return collection + @_api.make_keyword_only("3.9", "gridsize") @_preprocess_data(replace_names=["x", "y", "C"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def hexbin(self, x, y, C=None, gridsize=100, bins=None, xscale='linear', yscale='linear', extent=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, edgecolors='face', reduce_C_function=np.mean, mincnt=None, marginals=False, - **kwargs): + colorizer=None, **kwargs): """ Make a 2D hexagonal binning plot of points *x*, *y*. @@ -5018,7 +5137,7 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, A `.PolyCollection` defining the hexagonal bins. - `.PolyCollection.get_offsets` contains a Mx2 array containing - the x, y positions of the M hexagon centers. + the x, y positions of the M hexagon centers in data coordinates. - `.PolyCollection.get_array` contains the values of the M hexagons. @@ -5066,6 +5185,8 @@ def reduce_C_function(C: array) -> float input. Changing *mincnt* will adjust the cutoff, and if set to 0 will pass empty input to the reduction function. + %(colorizer_doc)s + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -5196,7 +5317,7 @@ def reduce_C_function(C: array) -> float linewidths = [mpl.rcParams['patch.linewidth']] if xscale == 'log' or yscale == 'log': - polygons = np.expand_dims(polygon, 0) + np.expand_dims(offsets, 1) + polygons = np.expand_dims(polygon, 0) if xscale == 'log': polygons[:, :, 0] = 10.0 ** polygons[:, :, 0] xmin = 10.0 ** xmin @@ -5207,20 +5328,16 @@ def reduce_C_function(C: array) -> float ymin = 10.0 ** ymin ymax = 10.0 ** ymax self.set_yscale(yscale) - collection = mcoll.PolyCollection( - polygons, - edgecolors=edgecolors, - linewidths=linewidths, - ) else: - collection = mcoll.PolyCollection( - [polygon], - edgecolors=edgecolors, - linewidths=linewidths, - offsets=offsets, - offset_transform=mtransforms.AffineDeltaTransform( - self.transData), - ) + polygons = [polygon] + + collection = mcoll.PolyCollection( + polygons, + edgecolors=edgecolors, + linewidths=linewidths, + offsets=offsets, + offset_transform=mtransforms.AffineDeltaTransform(self.transData) + ) # Set normalizer if bins is 'log' if cbook._str_equal(bins, 'log'): @@ -5232,11 +5349,6 @@ def reduce_C_function(C: array) -> float vmin = vmax = None bins = None - # autoscale the norm with current accum values if it hasn't been set - if norm is not None: - if norm.vmin is None and norm.vmax is None: - norm.autoscale(accum) - if bins is not None: if not np.iterable(bins): minimum, maximum = min(accum), max(accum) @@ -5245,13 +5357,23 @@ def reduce_C_function(C: array) -> float bins = np.sort(bins) accum = bins.searchsorted(accum) + if colorizer: + collection._set_colorizer_check_keywords(colorizer, cmap=cmap, + norm=norm, vmin=vmin, + vmax=vmax) + else: + collection.set_cmap(cmap) + collection.set_norm(norm) collection.set_array(accum) - collection.set_cmap(cmap) - collection.set_norm(norm) collection.set_alpha(alpha) collection._internal_update(kwargs) collection._scale_norm(norm, vmin, vmax) + # autoscale the norm with current accum values if it hasn't been set + if norm is not None: + if collection.norm.vmin is None and collection.norm.vmax is None: + collection.norm.autoscale() + corners = ((xmin, ymin), (xmax, ymax)) self.update_datalim(corners) self._request_autoscale_view(tight=True) @@ -5316,7 +5438,7 @@ def on_changed(collection): return collection - @_docstring.dedent_interpd + @_docstring.interpd def arrow(self, x, y, dx, dy, **kwargs): """ Add an arrow to the Axes. @@ -5371,7 +5493,7 @@ def _quiver_units(self, args, kwargs): # args can be a combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def quiver(self, *args, **kwargs): """%(quiver_doc)s""" # Make sure units are handled for x and y values @@ -5383,7 +5505,7 @@ def quiver(self, *args, **kwargs): # args can be some combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def barbs(self, *args, **kwargs): """%(barbs_doc)s""" # Make sure units are handled for x and y values @@ -5506,18 +5628,18 @@ def _fill_between_x_or_y( i.e. constant in between *{ind}*. The value determines where the step will occur: - - 'pre': The y value is continued constantly to the left from - every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the - value ``y[i]``. + - 'pre': The {dep} value is continued constantly to the left from + every *{ind}* position, i.e. the interval ``({ind}[i-1], {ind}[i]]`` + has the value ``{dep}[i]``. - 'post': The y value is continued constantly to the right from - every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the - value ``y[i]``. - - 'mid': Steps occur half-way between the *x* positions. + every *{ind}* position, i.e. the interval ``[{ind}[i], {ind}[i+1])`` + has the value ``{dep}[i]``. + - 'mid': Steps occur half-way between the *{ind}* positions. Returns ------- - `.PolyCollection` - A `.PolyCollection` containing the plotted polygons. + `.FillBetweenPolyCollection` + A `.FillBetweenPolyCollection` containing the plotted polygons. Other Parameters ---------------- @@ -5525,124 +5647,39 @@ def _fill_between_x_or_y( DATA_PARAMETER_PLACEHOLDER **kwargs - All other keyword arguments are passed on to `.PolyCollection`. - They control the `.Polygon` properties: + All other keyword arguments are passed on to + `.FillBetweenPolyCollection`. They control the `.Polygon` properties: - %(PolyCollection:kwdoc)s + %(FillBetweenPolyCollection:kwdoc)s See Also -------- fill_between : Fill between two sets of y-values. fill_betweenx : Fill between two sets of x-values. """ - - dep_dir = {"x": "y", "y": "x"}[ind_dir] + dep_dir = mcoll.FillBetweenPolyCollection._f_dir_from_t(ind_dir) if not mpl.rcParams["_internal.classic_mode"]: kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection) if not any(c in kwargs for c in ("color", "facecolor")): - kwargs["facecolor"] = \ - self._get_patches_for_fill.get_next_color() - - # Handle united data, such as dates - ind, dep1, dep2 = map( - ma.masked_invalid, self._process_unit_info( - [(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs)) + kwargs["facecolor"] = self._get_patches_for_fill.get_next_color() - for name, array in [ - (ind_dir, ind), (f"{dep_dir}1", dep1), (f"{dep_dir}2", dep2)]: - if array.ndim > 1: - raise ValueError(f"{name!r} is not 1-dimensional") - - if where is None: - where = True - else: - where = np.asarray(where, dtype=bool) - if where.size != ind.size: - raise ValueError(f"where size ({where.size}) does not match " - f"{ind_dir} size ({ind.size})") - where = where & ~functools.reduce( - np.logical_or, map(np.ma.getmaskarray, [ind, dep1, dep2])) - - ind, dep1, dep2 = np.broadcast_arrays( - np.atleast_1d(ind), dep1, dep2, subok=True) - - polys = [] - for idx0, idx1 in cbook.contiguous_regions(where): - indslice = ind[idx0:idx1] - dep1slice = dep1[idx0:idx1] - dep2slice = dep2[idx0:idx1] - if step is not None: - step_func = cbook.STEP_LOOKUP_MAP["steps-" + step] - indslice, dep1slice, dep2slice = \ - step_func(indslice, dep1slice, dep2slice) - - if not len(indslice): - continue + ind, dep1, dep2 = self._fill_between_process_units( + ind_dir, dep_dir, ind, dep1, dep2, **kwargs) - N = len(indslice) - pts = np.zeros((2 * N + 2, 2)) - - if interpolate: - def get_interp_point(idx): - im1 = max(idx - 1, 0) - ind_values = ind[im1:idx+1] - diff_values = dep1[im1:idx+1] - dep2[im1:idx+1] - dep1_values = dep1[im1:idx+1] - - if len(diff_values) == 2: - if np.ma.is_masked(diff_values[1]): - return ind[im1], dep1[im1] - elif np.ma.is_masked(diff_values[0]): - return ind[idx], dep1[idx] - - diff_order = diff_values.argsort() - diff_root_ind = np.interp( - 0, diff_values[diff_order], ind_values[diff_order]) - ind_order = ind_values.argsort() - diff_root_dep = np.interp( - diff_root_ind, - ind_values[ind_order], dep1_values[ind_order]) - return diff_root_ind, diff_root_dep - - start = get_interp_point(idx0) - end = get_interp_point(idx1) - else: - # Handle scalar dep2 (e.g. 0): the fill should go all - # the way down to 0 even if none of the dep1 sample points do. - start = indslice[0], dep2slice[0] - end = indslice[-1], dep2slice[-1] - - pts[0] = start - pts[N + 1] = end - - pts[1:N+1, 0] = indslice - pts[1:N+1, 1] = dep1slice - pts[N+2:, 0] = indslice[::-1] - pts[N+2:, 1] = dep2slice[::-1] - - if ind_dir == "y": - pts = pts[:, ::-1] - - polys.append(pts) - - collection = mcoll.PolyCollection(polys, **kwargs) - - # now update the datalim and autoscale - pts = np.vstack([np.hstack([ind[where, None], dep1[where, None]]), - np.hstack([ind[where, None], dep2[where, None]])]) - if ind_dir == "y": - pts = pts[:, ::-1] - - up_x = up_y = True - if "transform" in kwargs: - up_x, up_y = kwargs["transform"].contains_branch_seperately(self.transData) - self.update_datalim(pts, updatex=up_x, updatey=up_y) + collection = mcoll.FillBetweenPolyCollection( + ind_dir, ind, dep1, dep2, + where=where, interpolate=interpolate, step=step, **kwargs) - self.add_collection(collection, autolim=False) + self.add_collection(collection) self._request_autoscale_view() return collection + def _fill_between_process_units(self, ind_dir, dep_dir, ind, dep1, dep2, **kwargs): + """Handle united data, such as dates.""" + return map(np.ma.masked_invalid, self._process_unit_info( + [(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs)) + def fill_between(self, x, y1, y2=0, where=None, interpolate=False, step=None, **kwargs): return self._fill_between_x_or_y( @@ -5654,7 +5691,7 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, dir="horizontal", ind="x", dep="y" ) fill_between = _preprocess_data( - _docstring.dedent_interpd(fill_between), + _docstring.interpd(fill_between), replace_names=["x", "y1", "y2", "where"]) def fill_betweenx(self, y, x1, x2=0, where=None, @@ -5668,7 +5705,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, dir="vertical", ind="y", dep="x" ) fill_betweenx = _preprocess_data( - _docstring.dedent_interpd(fill_betweenx), + _docstring.interpd(fill_betweenx), replace_names=["y", "x1", "x2", "where"]) #### plotting z(x, y): imshow, pcolor and relatives, contour @@ -5677,7 +5714,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, @_docstring.interpd def imshow(self, X, cmap=None, norm=None, *, aspect=None, interpolation=None, alpha=None, - vmin=None, vmax=None, origin=None, extent=None, + vmin=None, vmax=None, colorizer=None, origin=None, extent=None, interpolation_stage=None, filternorm=True, filterrad=4.0, resample=None, url=None, **kwargs): """ @@ -5725,6 +5762,10 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, This parameter is ignored if *X* is RGB(A). + %(colorizer_doc)s + + This parameter is ignored if *X* is RGB(A). + aspect : {'equal', 'auto'} or float or None, default: None The aspect ratio of the Axes. This parameter is particularly relevant for images since it determines whether data pixels are @@ -5748,7 +5789,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, interpolation : str, default: :rc:`image.interpolation` The interpolation method used. - Supported values are 'none', 'antialiased', 'nearest', 'bilinear', + Supported values are 'none', 'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman'. @@ -5763,7 +5804,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, pdf, and svg viewers may display these raw pixels differently. On other backends, 'none' is the same as 'nearest'. - If *interpolation* is the default 'antialiased', then 'nearest' + If *interpolation* is the default 'auto', then 'nearest' interpolation is used if the image is upsampled by more than a factor of three (i.e. the number of display pixels is at least three times the size of the data array). If the upsampling rate is @@ -5781,11 +5822,20 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, which can be set by *filterrad*. Additionally, the antigrain image resize filter is controlled by the parameter *filternorm*. - interpolation_stage : {'data', 'rgba'}, default: 'data' - If 'data', interpolation - is carried out on the data provided by the user. If 'rgba', the - interpolation is carried out after the colormapping has been - applied (visual interpolation). + interpolation_stage : {'auto', 'data', 'rgba'}, default: 'auto' + Supported values: + + - 'data': Interpolation is carried out on the data provided by the user + This is useful if interpolating between pixels during upsampling. + - 'rgba': The interpolation is carried out in RGBA-space after the + color-mapping has been applied. This is useful if downsampling and + combining pixels visually. + - 'auto': Select a suitable interpolation stage automatically. This uses + 'rgba' when downsampling, or upsampling at a rate less than 3, and + 'data' when upsampling at a higher rate. + + See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for + a discussion of image antialiasing. alpha : float or array-like, optional The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -5878,7 +5928,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, `~matplotlib.pyplot.imshow` expects RGB images adopting the straight (unassociated) alpha representation. """ - im = mimage.AxesImage(self, cmap=cmap, norm=norm, + im = mimage.AxesImage(self, cmap=cmap, norm=norm, colorizer=colorizer, interpolation=interpolation, origin=origin, extent=extent, filternorm=filternorm, filterrad=filterrad, resample=resample, @@ -5897,6 +5947,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, if im.get_clip_path() is None: # image does not already have clipping set, clip to Axes patch im.set_clip_path(self.patch) + im._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) im._scale_norm(norm, vmin, vmax) im.set_url(url) @@ -5931,16 +5982,13 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): else: X, Y = np.meshgrid(np.arange(ncols + 1), np.arange(nrows + 1)) shading = 'flat' - C = cbook.safe_masked_invalid(C, copy=True) - return X, Y, C, shading - - if len(args) == 3: + elif len(args) == 3: # Check x and y for bad data... C = np.asanyarray(args[2]) # unit conversion allows e.g. datetime objects as axis values X, Y = args[:2] X, Y = self._process_unit_info([("x", X), ("y", Y)], kwargs) - X, Y = [cbook.safe_masked_invalid(a, copy=True) for a in [X, Y]] + X, Y = (cbook.safe_masked_invalid(a, copy=True) for a in [X, Y]) if funcname == 'pcolormesh': if np.ma.is_masked(X) or np.ma.is_masked(Y): @@ -5985,11 +6033,15 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): # grid is specified at the center, so define corners # at the midpoints between the grid centers and then use the # flat algorithm. - def _interp_grid(X): - # helper for below + def _interp_grid(X, require_monotonicity=False): + # helper for below. To ensure the cell edges are calculated + # correctly, when expanding columns, the monotonicity of + # X coords needs to be checked. When expanding rows, the + # monotonicity of Y coords needs to be checked. if np.shape(X)[1] > 1: dX = np.diff(X, axis=1) * 0.5 - if not (np.all(dX >= 0) or np.all(dX <= 0)): + if (require_monotonicity and + not (np.all(dX >= 0) or np.all(dX <= 0))): _api.warn_external( f"The input coordinates to {funcname} are " "interpreted as cell centers, but are not " @@ -6009,29 +6061,31 @@ def _interp_grid(X): return X if ncols == Nx: - X = _interp_grid(X) + X = _interp_grid(X, require_monotonicity=True) Y = _interp_grid(Y) if nrows == Ny: X = _interp_grid(X.T).T - Y = _interp_grid(Y.T).T + Y = _interp_grid(Y.T, require_monotonicity=True).T shading = 'flat' C = cbook.safe_masked_invalid(C, copy=True) return X, Y, C, shading @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, - vmin=None, vmax=None, **kwargs): + vmin=None, vmax=None, colorizer=None, **kwargs): r""" Create a pseudocolor plot with a non-regular rectangular grid. Call signature:: - pcolor([X, Y,] C, **kwargs) + pcolor([X, Y,] C, /, **kwargs) *X* and *Y* can be used to specify the corners of the quadrilaterals. + The arguments *X*, *Y*, *C* are positional-only. + .. hint:: ``pcolor()`` can be very slow for large arrays. In most @@ -6098,6 +6152,8 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, %(vmin_vmax_doc)s + %(colorizer_doc)s + edgecolors : {'none', None, 'face', color, color sequence}, optional The color of the edges. Defaults to 'none'. Possible values: @@ -6205,7 +6261,9 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, coords = stack([X, Y], axis=-1) collection = mcoll.PolyQuadMesh( - coords, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) + coords, array=C, cmap=cmap, norm=norm, colorizer=colorizer, + alpha=alpha, **kwargs) + collection._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) collection._scale_norm(norm, vmin, vmax) # Transform from native to data coordinates? @@ -6235,18 +6293,21 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, return collection @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - vmax=None, shading=None, antialiased=False, **kwargs): + vmax=None, colorizer=None, shading=None, antialiased=False, + **kwargs): """ Create a pseudocolor plot with a non-regular rectangular grid. Call signature:: - pcolormesh([X, Y,] C, **kwargs) + pcolormesh([X, Y,] C, /, **kwargs) *X* and *Y* can be used to specify the corners of the quadrilaterals. + The arguments *X*, *Y*, *C* are positional-only. + .. hint:: `~.Axes.pcolormesh` is similar to `~.Axes.pcolor`. It is much faster @@ -6304,6 +6365,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, %(vmin_vmax_doc)s + %(colorizer_doc)s + edgecolors : {'none', None, 'face', color, color sequence}, optional The color of the edges. Defaults to 'none'. Possible values: @@ -6433,7 +6496,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection = mcoll.QuadMesh( coords, antialiased=antialiased, shading=shading, - array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) + array=C, cmap=cmap, norm=norm, colorizer=colorizer, alpha=alpha, **kwargs) + collection._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) collection._scale_norm(norm, vmin, vmax) coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y @@ -6460,15 +6524,17 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return collection @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - vmax=None, **kwargs): + vmax=None, colorizer=None, **kwargs): """ Create a pseudocolor plot with a non-regular rectangular grid. Call signature:: - ax.pcolorfast([X, Y], C, /, **kwargs) + ax.pcolorfast([X, Y], C, /, **kwargs) + + The arguments *X*, *Y*, *C* are positional-only. This method is similar to `~.Axes.pcolor` and `~.Axes.pcolormesh`. It's designed to provide the fastest pcolor-type plotting with the @@ -6478,12 +6544,12 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, .. warning:: - This method is experimental. Compared to `~.Axes.pcolor` or - `~.Axes.pcolormesh` it has some limitations: + This method is experimental. Compared to `~.Axes.pcolor` or + `~.Axes.pcolormesh` it has some limitations: - - It supports only flat shading (no outlines) - - It lacks support for log scaling of the axes. - - It does not have a pyplot wrapper. + - It supports only flat shading (no outlines) + - It lacks support for log scaling of the axes. + - It does not have a pyplot wrapper. Parameters ---------- @@ -6546,6 +6612,10 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, This parameter is ignored if *C* is RGB(A). + %(colorizer_doc)s + + This parameter is ignored if *C* is RGB(A). + alpha : float, default: None The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -6585,6 +6655,15 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, if x.size == 2 and y.size == 2: style = "image" else: + if x.size != nc + 1: + raise ValueError( + f"Length of X ({x.size}) must be one larger than the " + f"number of columns in C ({nc})") + if y.size != nr + 1: + raise ValueError( + f"Length of Y ({y.size}) must be one larger than the " + f"number of rows in C ({nr})" + ) dx = np.diff(x) dy = np.diff(y) if (np.ptp(dx) < 0.01 * abs(dx.mean()) and @@ -6602,6 +6681,8 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, else: raise _api.nargs_error('pcolorfast', '1 or 3', len(args)) + mcolorizer.ColorizingArtist._check_exclusionary_keywords(colorizer, vmin=vmin, + vmax=vmax) if style == "quadmesh": # data point in each cell is value at lower left corner coords = np.stack([x, y], axis=-1) @@ -6609,7 +6690,7 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, raise ValueError("C must be 2D or 3D") collection = mcoll.QuadMesh( coords, array=C, - alpha=alpha, cmap=cmap, norm=norm, + alpha=alpha, cmap=cmap, norm=norm, colorizer=colorizer, antialiased=False, edgecolors="none") self.add_collection(collection, autolim=False) xl, xr, yb, yt = x.min(), x.max(), y.min(), y.max() @@ -6619,15 +6700,15 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, extent = xl, xr, yb, yt = x[0], x[-1], y[0], y[-1] if style == "image": im = mimage.AxesImage( - self, cmap=cmap, norm=norm, + self, cmap=cmap, norm=norm, colorizer=colorizer, data=C, alpha=alpha, extent=extent, interpolation='nearest', origin='lower', **kwargs) elif style == "pcolorimage": im = mimage.PcolorImage( self, x, y, C, - cmap=cmap, norm=norm, alpha=alpha, extent=extent, - **kwargs) + cmap=cmap, norm=norm, colorizer=colorizer, alpha=alpha, + extent=extent, **kwargs) self.add_image(im) ret = im @@ -6645,14 +6726,16 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return ret @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def contour(self, *args, **kwargs): """ Plot contour lines. Call signature:: - contour([X, Y,] Z, [levels], **kwargs) + contour([X, Y,] Z, /, [levels], **kwargs) + + The arguments *X*, *Y*, *Z* are positional-only. %(contour_doc)s """ kwargs['filled'] = False @@ -6661,14 +6744,16 @@ def contour(self, *args, **kwargs): return contours @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def contourf(self, *args, **kwargs): """ Plot filled contours. Call signature:: - contourf([X, Y,] Z, [levels], **kwargs) + contourf([X, Y,] Z, /, [levels], **kwargs) + + The arguments *X*, *Y*, *Z* are positional-only. %(contour_doc)s """ kwargs['filled'] = True @@ -6698,6 +6783,7 @@ def clabel(self, CS, levels=None, **kwargs): #### Data analysis + @_api.make_keyword_only("3.9", "range") @_preprocess_data(replace_names=["x", 'weights'], label_namer="x") def hist(self, x, bins=None, range=None, density=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', @@ -6875,7 +6961,13 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, DATA_PARAMETER_PLACEHOLDER **kwargs - `~matplotlib.patches.Patch` properties + `~matplotlib.patches.Patch` properties. The following properties + additionally accept a sequence of values corresponding to the + datasets in *x*: + *edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*. + + .. versionadded:: 3.10 + Allowing sequences of values in above listed Patch properties. See Also -------- @@ -7148,15 +7240,35 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, # If None, make all labels None (via zip_longest below); otherwise, # cast each element to str, but keep a single str as it. labels = [] if label is None else np.atleast_1d(np.asarray(label, str)) + + if histtype == "step": + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get('edgecolor', + colors))) + else: + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get("edgecolor", None))) + + facecolors = itertools.cycle(np.atleast_1d(kwargs.get('facecolor', colors))) + hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None))) + linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None))) + linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None))) + for patch, lbl in itertools.zip_longest(patches, labels): - if patch: - p = patch[0] + if not patch: + continue + p = patch[0] + kwargs.update({ + 'hatch': next(hatches), + 'linewidth': next(linewidths), + 'linestyle': next(linestyles), + 'edgecolor': next(edgecolors), + 'facecolor': next(facecolors), + }) + p._internal_update(kwargs) + if lbl is not None: + p.set_label(lbl) + for p in patch[1:]: p._internal_update(kwargs) - if lbl is not None: - p.set_label(lbl) - for p in patch[1:]: - p._internal_update(kwargs) - p.set_label('_nolegend_') + p.set_label('_nolegend_') if nx == 1: return tops[0], bins, patches[0] @@ -7194,9 +7306,16 @@ def stairs(self, values, edges=None, *, True or an array is passed to *baseline*, a closed path is drawn. + If None, then drawn as an unclosed Path. + fill : bool, default: False Whether the area under the step curve should be filled. + Passing both ``fill=True` and ``baseline=None`` will likely result in + undesired filling: the first and last points will be connected + with a straight line and the fill will be between this line and the stairs. + + Returns ------- StepPatch : `~matplotlib.patches.StepPatch` @@ -7234,19 +7353,30 @@ def stairs(self, values, edges=None, *, fill=fill, **kwargs) self.add_patch(patch) - if baseline is None: - baseline = 0 - if orientation == 'vertical': - patch.sticky_edges.y.append(np.min(baseline)) - self.update_datalim([(edges[0], np.min(baseline))]) - else: - patch.sticky_edges.x.append(np.min(baseline)) - self.update_datalim([(np.min(baseline), edges[0])]) + if baseline is None and fill: + _api.warn_external( + f"Both {baseline=} and {fill=} have been passed. " + "baseline=None is only intended for unfilled stair plots. " + "Because baseline is None, the Path used to draw the stairs will " + "not be closed, thus because fill is True the polygon will be closed " + "by drawing an (unstroked) edge from the first to last point. It is " + "very likely that the resulting fill patterns is not the desired " + "result." + ) + + if baseline is not None: + if orientation == 'vertical': + patch.sticky_edges.y.append(np.min(baseline)) + self.update_datalim([(edges[0], np.min(baseline))]) + else: + patch.sticky_edges.x.append(np.min(baseline)) + self.update_datalim([(np.min(baseline), edges[0])]) self._request_autoscale_view() return patch + @_api.make_keyword_only("3.9", "range") @_preprocess_data(replace_names=["x", "y", "weights"]) - @_docstring.dedent_interpd + @_docstring.interpd def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, cmin=None, cmax=None, **kwargs): """ @@ -7311,6 +7441,8 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, %(vmin_vmax_doc)s + %(colorizer_doc)s + alpha : ``0 <= scalar <= 1`` or ``None``, optional The alpha blending value. @@ -7353,7 +7485,7 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, return h, xedges, yedges, pc @_preprocess_data(replace_names=["x", "weights"], label_namer="x") - @_docstring.dedent_interpd + @_docstring.interpd def ecdf(self, x, weights=None, *, complementary=False, orientation="vertical", compress=False, **kwargs): """ @@ -7454,8 +7586,9 @@ def ecdf(self, x, weights=None, *, complementary=False, line.sticky_edges.x[:] = [0, 1] return line + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7565,8 +7698,9 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxx, freqs, line + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7667,8 +7801,9 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxy, freqs, line + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, scale=None, **kwargs): @@ -7753,8 +7888,9 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, line + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -7822,8 +7958,9 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -7891,8 +8028,9 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"]) - @_docstring.dedent_interpd + @_docstring.interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None, **kwargs): @@ -7955,8 +8093,9 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, return cxy, freqs + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, cmap=None, xextent=None, pad_to=None, sides=None, @@ -8014,6 +8153,11 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + vmin, vmax : float, optional + vmin and vmax define the data range that the colormap covers. + By default, the colormap covers the complete value range of the + data. + **kwargs Additional keyword arguments are passed on to `~.axes.Axes.imshow` which makes the specgram image. The origin keyword argument @@ -8111,7 +8255,8 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, return spec, freqs, t, im - @_docstring.dedent_interpd + @_api.make_keyword_only("3.9", "precision") + @_docstring.interpd def spy(self, Z, precision=0, marker=None, markersize=None, aspect='equal', origin="upper", **kwargs): """ @@ -8301,10 +8446,12 @@ def matshow(self, Z, **kwargs): mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True)) return im + @_api.make_keyword_only("3.9", "vert") @_preprocess_data(replace_names=["dataset"]) - def violinplot(self, dataset, positions=None, vert=True, widths=0.5, - showmeans=False, showextrema=True, showmedians=False, - quantiles=None, points=100, bw_method=None, side='both'): + def violinplot(self, dataset, positions=None, vert=None, + orientation='vertical', widths=0.5, showmeans=False, + showextrema=True, showmedians=False, quantiles=None, + points=100, bw_method=None, side='both',): """ Make a violin plot. @@ -8316,44 +8463,56 @@ def violinplot(self, dataset, positions=None, vert=True, widths=0.5, Parameters ---------- dataset : Array or a sequence of vectors. - The input data. + The input data. positions : array-like, default: [1, 2, ..., n] - The positions of the violins; i.e. coordinates on the x-axis for - vertical violins (or y-axis for horizontal violins). + The positions of the violins; i.e. coordinates on the x-axis for + vertical violins (or y-axis for horizontal violins). + + vert : bool, optional + .. deprecated:: 3.10 + Use *orientation* instead. + + If this is given during the deprecation period, it overrides + the *orientation* parameter. - vert : bool, default: True. - If true, creates a vertical violin plot. - Otherwise, creates a horizontal violin plot. + If True, plots the violins vertically. + If False, plots the violins horizontally. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + If 'horizontal', plots the violins horizontally. + Otherwise, plots the violins vertically. + + .. versionadded:: 3.10 widths : float or array-like, default: 0.5 - The maximum width of each violin in units of the *positions* axis. - The default is 0.5, which is half the available space when using default - *positions*. + The maximum width of each violin in units of the *positions* axis. + The default is 0.5, which is half the available space when using default + *positions*. showmeans : bool, default: False - Whether to show the mean with a line. + Whether to show the mean with a line. showextrema : bool, default: True - Whether to show extrema with a line. + Whether to show extrema with a line. showmedians : bool, default: False - Whether to show the median with a line. + Whether to show the median with a line. quantiles : array-like, default: None - If not None, set a list of floats in interval [0, 1] for each violin, - which stands for the quantiles that will be rendered for that - violin. + If not None, set a list of floats in interval [0, 1] for each violin, + which stands for the quantiles that will be rendered for that + violin. points : int, default: 100 - The number of points to evaluate each of the gaussian kernel density - estimations at. + The number of points to evaluate each of the gaussian kernel density + estimations at. bw_method : {'scott', 'silverman'} or float or callable, default: 'scott' - The method used to calculate the estimator bandwidth. 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. + The method used to calculate the estimator bandwidth. 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. side : {'both', 'low', 'high'}, default: 'both' 'both' plots standard violins. 'low'/'high' only @@ -8365,31 +8524,31 @@ def violinplot(self, dataset, positions=None, vert=True, widths=0.5, Returns ------- dict - A dictionary mapping each component of the violinplot to a - list of the corresponding collection instances created. The - dictionary has the following keys: + A dictionary mapping each component of the violinplot to a + list of the corresponding collection instances created. The + dictionary has the following keys: - - ``bodies``: A list of the `~.collections.PolyCollection` - instances containing the filled area of each violin. + - ``bodies``: A list of the `~.collections.PolyCollection` + instances containing the filled area of each violin. - - ``cmeans``: A `~.collections.LineCollection` instance that marks - the mean values of each of the violin's distribution. + - ``cmeans``: A `~.collections.LineCollection` instance that marks + the mean values of each of the violin's distribution. - - ``cmins``: A `~.collections.LineCollection` instance that marks - the bottom of each violin's distribution. + - ``cmins``: A `~.collections.LineCollection` instance that marks + the bottom of each violin's distribution. - - ``cmaxes``: A `~.collections.LineCollection` instance that marks - the top of each violin's distribution. + - ``cmaxes``: A `~.collections.LineCollection` instance that marks + the top of each violin's distribution. - - ``cbars``: A `~.collections.LineCollection` instance that marks - the centers of each violin's distribution. + - ``cbars``: A `~.collections.LineCollection` instance that marks + the centers of each violin's distribution. - - ``cmedians``: A `~.collections.LineCollection` instance that - marks the median values of each of the violin's distribution. + - ``cmedians``: A `~.collections.LineCollection` instance that + marks the median values of each of the violin's distribution. - - ``cquantiles``: A `~.collections.LineCollection` instance created - to identify the quantile values of each of the violin's - distribution. + - ``cquantiles``: A `~.collections.LineCollection` instance created + to identify the quantile values of each of the violin's + distribution. See Also -------- @@ -8409,11 +8568,14 @@ def _kde_method(X, coords): vpstats = cbook.violin_stats(dataset, _kde_method, points=points, quantiles=quantiles) return self.violin(vpstats, positions=positions, vert=vert, - widths=widths, showmeans=showmeans, - showextrema=showextrema, showmedians=showmedians, side=side) - - def violin(self, vpstats, positions=None, vert=True, widths=0.5, - showmeans=False, showextrema=True, showmedians=False, side='both'): + orientation=orientation, widths=widths, + showmeans=showmeans, showextrema=showextrema, + showmedians=showmedians, side=side) + + @_api.make_keyword_only("3.9", "vert") + def violin(self, vpstats, positions=None, vert=None, + orientation='vertical', widths=0.5, showmeans=False, + showextrema=True, showmedians=False, side='both'): """ Draw a violin plot from pre-computed statistics. @@ -8424,50 +8586,62 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, Parameters ---------- vpstats : list of dicts - A list of dictionaries containing stats for each violin plot. - Required keys are: + A list of dictionaries containing stats for each violin plot. + Required keys are: - - ``coords``: A list of scalars containing the coordinates that - the violin's kernel density estimate were evaluated at. + - ``coords``: A list of scalars containing the coordinates that + the violin's kernel density estimate were evaluated at. - - ``vals``: A list of scalars containing the values of the - kernel density estimate at each of the coordinates given - in *coords*. + - ``vals``: A list of scalars containing the values of the + kernel density estimate at each of the coordinates given + in *coords*. - - ``mean``: The mean value for this violin's dataset. + - ``mean``: The mean value for this violin's dataset. - - ``median``: The median value for this violin's dataset. + - ``median``: The median value for this violin's dataset. - - ``min``: The minimum value for this violin's dataset. + - ``min``: The minimum value for this violin's dataset. - - ``max``: The maximum value for this violin's dataset. + - ``max``: The maximum value for this violin's dataset. - Optional keys are: + Optional keys are: - - ``quantiles``: A list of scalars containing the quantile values - for this violin's dataset. + - ``quantiles``: A list of scalars containing the quantile values + for this violin's dataset. positions : array-like, default: [1, 2, ..., n] - The positions of the violins; i.e. coordinates on the x-axis for - vertical violins (or y-axis for horizontal violins). + The positions of the violins; i.e. coordinates on the x-axis for + vertical violins (or y-axis for horizontal violins). - vert : bool, default: True. - If true, plots the violins vertically. - Otherwise, plots the violins horizontally. + vert : bool, optional + .. deprecated:: 3.10 + Use *orientation* instead. + + If this is given during the deprecation period, it overrides + the *orientation* parameter. + + If True, plots the violins vertically. + If False, plots the violins horizontally. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + If 'horizontal', plots the violins horizontally. + Otherwise, plots the violins vertically. + + .. versionadded:: 3.10 widths : float or array-like, default: 0.5 - The maximum width of each violin in units of the *positions* axis. - The default is 0.5, which is half available space when using default - *positions*. + The maximum width of each violin in units of the *positions* axis. + The default is 0.5, which is half available space when using default + *positions*. showmeans : bool, default: False - Whether to show the mean with a line. + Whether to show the mean with a line. showextrema : bool, default: True - Whether to show extrema with a line. + Whether to show extrema with a line. showmedians : bool, default: False - Whether to show the median with a line. + Whether to show the median with a line. side : {'both', 'low', 'high'}, default: 'both' 'both' plots standard violins. 'low'/'high' only @@ -8476,35 +8650,35 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, Returns ------- dict - A dictionary mapping each component of the violinplot to a - list of the corresponding collection instances created. The - dictionary has the following keys: + A dictionary mapping each component of the violinplot to a + list of the corresponding collection instances created. The + dictionary has the following keys: - - ``bodies``: A list of the `~.collections.PolyCollection` - instances containing the filled area of each violin. + - ``bodies``: A list of the `~.collections.PolyCollection` + instances containing the filled area of each violin. - - ``cmeans``: A `~.collections.LineCollection` instance that marks - the mean values of each of the violin's distribution. + - ``cmeans``: A `~.collections.LineCollection` instance that marks + the mean values of each of the violin's distribution. - - ``cmins``: A `~.collections.LineCollection` instance that marks - the bottom of each violin's distribution. + - ``cmins``: A `~.collections.LineCollection` instance that marks + the bottom of each violin's distribution. - - ``cmaxes``: A `~.collections.LineCollection` instance that marks - the top of each violin's distribution. + - ``cmaxes``: A `~.collections.LineCollection` instance that marks + the top of each violin's distribution. - - ``cbars``: A `~.collections.LineCollection` instance that marks - the centers of each violin's distribution. + - ``cbars``: A `~.collections.LineCollection` instance that marks + the centers of each violin's distribution. - - ``cmedians``: A `~.collections.LineCollection` instance that - marks the median values of each of the violin's distribution. + - ``cmedians``: A `~.collections.LineCollection` instance that + marks the median values of each of the violin's distribution. - - ``cquantiles``: A `~.collections.LineCollection` instance created - to identify the quantiles values of each of the violin's - distribution. + - ``cquantiles``: A `~.collections.LineCollection` instance created + to identify the quantiles values of each of the violin's + distribution. See Also -------- - violin : + violinplot : Draw a violin plot from data instead of pre-computed statistics. """ @@ -8523,6 +8697,19 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, datashape_message = ("List of violinplot statistics and `{0}` " "values must have the same length") + # vert and orientation parameters are linked until vert's + # deprecation period expires. If both are selected, + # vert takes precedence. + if vert is not None: + _api.warn_deprecated( + "3.11", + name="vert: bool", + alternative="orientation: {'vertical', 'horizontal'}", + pending=True, + ) + orientation = 'vertical' if vert else 'horizontal' + _api.check_in_list(['horizontal', 'vertical'], orientation=orientation) + # Validate positions if positions is None: positions = range(1, N + 1) @@ -8551,7 +8738,7 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, fillcolor = linecolor = self._get_lines.get_next_color() # Check whether we are rendering vertically or horizontally - if vert: + if orientation == 'vertical': fill = self.fill_betweenx if side in ['low', 'high']: perp_lines = functools.partial(self.hlines, colors=linecolor, diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 76aaee77aff8..1877cc192b15 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -5,16 +5,19 @@ from matplotlib.artist import Artist from matplotlib.backend_bases import RendererBase from matplotlib.collections import ( Collection, + FillBetweenPolyCollection, LineCollection, PathCollection, PolyCollection, EventCollection, QuadMesh, ) +from matplotlib.colorizer import Colorizer from matplotlib.colors import Colormap, Normalize from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage +from matplotlib.inset import InsetIndicator from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase from matplotlib.lines import Line2D, AxLine @@ -22,7 +25,8 @@ from matplotlib.mlab import GaussianKDE from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch, Wedge from matplotlib.quiver import Quiver, QuiverKey, Barbs from matplotlib.text import Annotation, Text -from matplotlib.transforms import Transform, Bbox +from matplotlib.transforms import Transform +from matplotlib.typing import CoordsType import matplotlib.tri as mtri import matplotlib.table as mtable import matplotlib.stackplot as mstack @@ -74,38 +78,38 @@ class Axes(_AxesBase): ) -> Axes: ... def indicate_inset( self, - bounds: tuple[float, float, float, float], + bounds: tuple[float, float, float, float] | None = ..., inset_ax: Axes | None = ..., *, transform: Transform | None = ..., facecolor: ColorType = ..., edgecolor: ColorType = ..., alpha: float = ..., - zorder: float = ..., + zorder: float | None = ..., **kwargs - ) -> Rectangle: ... - def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ... + ) -> InsetIndicator: ... + def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> InsetIndicator: ... def secondary_xaxis( self, location: Literal["top", "bottom"] | float, - *, functions: tuple[ Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike] ] | Transform | None = ..., + *, transform: Transform | None = ..., **kwargs ) -> SecondaryAxis: ... def secondary_yaxis( self, location: Literal["left", "right"] | float, - *, functions: tuple[ Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike] ] | Transform | None = ..., + *, transform: Transform | None = ..., **kwargs ) -> SecondaryAxis: ... @@ -122,17 +126,8 @@ class Axes(_AxesBase): text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = ..., - xycoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] = ..., - textcoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] - | None = ..., + xycoords: CoordsType = ..., + textcoords: CoordsType | None = ..., arrowprops: dict[str, Any] | None = ..., annotation_clip: bool | None = ..., **kwargs @@ -166,8 +161,8 @@ class Axes(_AxesBase): xmax: float | ArrayLike, colors: ColorType | Sequence[ColorType] | None = ..., linestyles: LineStyleType = ..., - label: str = ..., *, + label: str = ..., data=..., **kwargs ) -> LineCollection: ... @@ -178,14 +173,15 @@ class Axes(_AxesBase): ymax: float | ArrayLike, colors: ColorType | Sequence[ColorType] | None = ..., linestyles: LineStyleType = ..., - label: str = ..., *, + label: str = ..., data=..., **kwargs ) -> LineCollection: ... def eventplot( self, positions: ArrayLike | Sequence[ArrayLike], + *, orientation: Literal["horizontal", "vertical"] = ..., lineoffsets: float | Sequence[float] = ..., linelengths: float | Sequence[float] = ..., @@ -193,7 +189,6 @@ class Axes(_AxesBase): colors: ColorType | Sequence[ColorType] | None = ..., alpha: float | Sequence[float] | None = ..., linestyles: LineStyleType | Sequence[LineStyleType] = ..., - *, data=..., **kwargs ) -> EventCollection: ... @@ -202,7 +197,7 @@ class Axes(_AxesBase): *args: float | ArrayLike | str, scalex: bool = ..., scaley: bool = ..., - data = ..., + data=..., **kwargs ) -> list[Line2D]: ... def plot_date( @@ -227,12 +222,12 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, normed: bool = ..., detrend: Callable[[ArrayLike], ArrayLike] = ..., usevlines: bool = ..., maxlags: int = ..., - *, - data = ..., + data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ... def step( @@ -241,7 +236,7 @@ class Axes(_AxesBase): y: ArrayLike, *args, where: Literal["pre", "post", "mid"] = ..., - data = ..., + data=..., **kwargs ) -> list[Line2D]: ... def bar( @@ -252,7 +247,7 @@ class Axes(_AxesBase): bottom: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data = ..., + data=..., **kwargs ) -> BarContainer: ... def barh( @@ -263,7 +258,7 @@ class Axes(_AxesBase): left: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data = ..., + data=..., **kwargs ) -> BarContainer: ... def bar_label( @@ -300,6 +295,7 @@ class Axes(_AxesBase): def pie( self, x: ArrayLike, + *, explode: ArrayLike | None = ..., labels: Sequence[str] | None = ..., colors: ColorType | Sequence[ColorType] | None = ..., @@ -315,7 +311,6 @@ class Axes(_AxesBase): center: tuple[float, float] = ..., frame: bool = ..., rotatelabels: bool = ..., - *, normalize: bool = ..., hatch: str | Sequence[str] | None = ..., data=..., @@ -329,6 +324,7 @@ class Axes(_AxesBase): yerr: float | ArrayLike | None = ..., xerr: float | ArrayLike | None = ..., fmt: str = ..., + *, ecolor: ColorType | None = ..., elinewidth: float | None = ..., capsize: float | None = ..., @@ -339,16 +335,17 @@ class Axes(_AxesBase): xuplims: bool | ArrayLike = ..., errorevery: int | tuple[int, int] = ..., capthick: float | None = ..., - *, data=..., **kwargs ) -> ErrorbarContainer: ... def boxplot( self, x: ArrayLike | Sequence[ArrayLike], + *, notch: bool | None = ..., sym: str | None = ..., vert: bool | None = ..., + orientation: Literal["vertical", "horizontal"] = ..., whis: float | tuple[float, float] | None = ..., positions: ArrayLike | None = ..., widths: float | ArrayLike | None = ..., @@ -373,15 +370,16 @@ class Axes(_AxesBase): zorder: float | None = ..., capwidths: float | ArrayLike | None = ..., label: Sequence[str] | None = ..., - *, data=..., ) -> dict[str, Any]: ... def bxp( self, bxpstats: Sequence[dict[str, Any]], positions: ArrayLike | None = ..., + *, widths: float | ArrayLike | None = ..., - vert: bool = ..., + vert: bool | None = ..., + orientation: Literal["vertical", "horizontal"] = ..., patch_artist: bool = ..., shownotches: bool = ..., showmeans: bool = ..., @@ -406,6 +404,7 @@ class Axes(_AxesBase): y: float | ArrayLike, s: float | ArrayLike | None = ..., c: ArrayLike | Sequence[ColorType] | ColorType | None = ..., + *, marker: MarkerType | None = ..., cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., @@ -413,8 +412,8 @@ class Axes(_AxesBase): vmax: float | None = ..., alpha: float | None = ..., linewidths: float | Sequence[float] | None = ..., - *, edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ..., + colorizer: Colorizer | None = ..., plotnonfinite: bool = ..., data=..., **kwargs @@ -424,6 +423,7 @@ class Axes(_AxesBase): x: ArrayLike, y: ArrayLike, C: ArrayLike | None = ..., + *, gridsize: int | tuple[int, int] = ..., bins: Literal["log"] | int | Sequence[float] | None = ..., xscale: Literal["linear", "log"] = ..., @@ -439,7 +439,7 @@ class Axes(_AxesBase): reduce_C_function: Callable[[np.ndarray | list[float]], float] = ..., mincnt: int | None = ..., marginals: bool = ..., - *, + colorizer: Colorizer | None = ..., data=..., **kwargs ) -> PolyCollection: ... @@ -463,7 +463,7 @@ class Axes(_AxesBase): *, data=..., **kwargs - ) -> PolyCollection: ... + ) -> FillBetweenPolyCollection: ... def fill_betweenx( self, y: ArrayLike, @@ -475,7 +475,7 @@ class Axes(_AxesBase): *, data=..., **kwargs - ) -> PolyCollection: ... + ) -> FillBetweenPolyCollection: ... def imshow( self, X: ArrayLike | PIL.Image.Image, @@ -487,9 +487,10 @@ class Axes(_AxesBase): alpha: float | ArrayLike | None = ..., vmin: float | None = ..., vmax: float | None = ..., + colorizer: Colorizer | None = ..., origin: Literal["upper", "lower"] | None = ..., extent: tuple[float, float, float, float] | None = ..., - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., filternorm: bool = ..., filterrad: float = ..., resample: bool | None = ..., @@ -506,6 +507,7 @@ class Axes(_AxesBase): cmap: str | Colormap | None = ..., vmin: float | None = ..., vmax: float | None = ..., + colorizer: Colorizer | None = ..., data=..., **kwargs ) -> Collection: ... @@ -517,6 +519,7 @@ class Axes(_AxesBase): cmap: str | Colormap | None = ..., vmin: float | None = ..., vmax: float | None = ..., + colorizer: Colorizer | None = ..., shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ..., antialiased: bool = ..., data=..., @@ -530,6 +533,7 @@ class Axes(_AxesBase): cmap: str | Colormap | None = ..., vmin: float | None = ..., vmax: float | None = ..., + colorizer: Colorizer | None = ..., data=..., **kwargs ) -> AxesImage | PcolorImage | QuadMesh: ... @@ -542,6 +546,7 @@ class Axes(_AxesBase): self, x: ArrayLike | Sequence[ArrayLike], bins: int | Sequence[float] | str | None = ..., + *, range: tuple[float, float] | None = ..., density: bool = ..., weights: ArrayLike | None = ..., @@ -555,7 +560,6 @@ class Axes(_AxesBase): color: ColorType | Sequence[ColorType] | None = ..., label: str | Sequence[str] | None = ..., stacked: bool = ..., - *, data=..., **kwargs ) -> tuple[ @@ -583,12 +587,12 @@ class Axes(_AxesBase): | tuple[int, int] | ArrayLike | tuple[ArrayLike, ArrayLike] = ..., + *, range: ArrayLike | None = ..., density: bool = ..., weights: ArrayLike | None = ..., cmin: float | None = ..., cmax: float | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: ... @@ -606,6 +610,7 @@ class Axes(_AxesBase): def psd( self, x: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -618,7 +623,6 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... @@ -626,6 +630,7 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -638,44 +643,43 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... def magnitude_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., scale: Literal["default", "linear", "dB"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def angle_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def phase_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... @@ -683,6 +687,7 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, NFFT: int = ..., Fs: float = ..., Fc: int = ..., @@ -693,13 +698,13 @@ class Axes(_AxesBase): pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] = ..., scale_by_freq: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray]: ... def specgram( self, x: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -717,13 +722,13 @@ class Axes(_AxesBase): scale: Literal["default", "linear", "dB"] | None = ..., vmin: float | None = ..., vmax: float | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: ... def spy( self, Z: ArrayLike, + *, precision: float | Literal["present"] = ..., marker: str | None = ..., markersize: float | None = ..., @@ -736,7 +741,9 @@ class Axes(_AxesBase): self, dataset: ArrayLike | Sequence[ArrayLike], positions: ArrayLike | None = ..., - vert: bool = ..., + *, + vert: bool | None = ..., + orientation: Literal["vertical", "horizontal"] = ..., widths: float | ArrayLike = ..., showmeans: bool = ..., showextrema: bool = ..., @@ -748,14 +755,15 @@ class Axes(_AxesBase): | Callable[[GaussianKDE], float] | None = ..., side: Literal["both", "low", "high"] = ..., - *, data=..., ) -> dict[str, Collection]: ... def violin( self, vpstats: Sequence[dict[str, Any]], positions: ArrayLike | None = ..., - vert: bool = ..., + *, + vert: bool | None = ..., + orientation: Literal["vertical", "horizontal"] = ..., widths: float | ArrayLike = ..., showmeans: bool = ..., showextrema: bool = ..., diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6b3f2750575c..4c5b18e9e843 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -115,7 +115,7 @@ def __call__(self, ax, renderer): # time as transSubfigure may otherwise change after this is evaluated. return mtransforms.TransformedBbox( mtransforms.Bbox.from_bounds(*self._bounds), - self._transform - ax.figure.transSubfigure) + self._transform - ax.get_figure(root=False).transSubfigure) def _process_plot_format(fmt, *, ambiguous_fmt_datakey=False): @@ -213,8 +213,9 @@ class _process_plot_var_args: an arbitrary number of *x*, *y*, *fmt* are allowed """ - def __init__(self, command='plot'): - self.command = command + def __init__(self, output='Line2D'): + _api.check_in_list(['Line2D', 'Polygon', 'coordinates'], output=output) + self.output = output self.set_prop_cycle(None) def set_prop_cycle(self, cycler): @@ -223,12 +224,12 @@ def set_prop_cycle(self, cycler): self._idx = 0 self._cycler_items = [*cycler] - def __call__(self, axes, *args, data=None, **kwargs): + def __call__(self, axes, *args, data=None, return_kwargs=False, **kwargs): axes._process_unit_info(kwargs=kwargs) for pos_only in "xy": if pos_only in kwargs: - raise _api.kwarg_error(self.command, pos_only) + raise _api.kwarg_error(inspect.stack()[1].function, pos_only) if not args: return @@ -294,7 +295,9 @@ def __call__(self, axes, *args, data=None, **kwargs): this += args[0], args = args[1:] yield from self._plot_args( - axes, this, kwargs, ambiguous_fmt_datakey=ambiguous_fmt_datakey) + axes, this, kwargs, ambiguous_fmt_datakey=ambiguous_fmt_datakey, + return_kwargs=return_kwargs + ) def get_next_color(self): """Return the next color in the cycle.""" @@ -329,13 +332,18 @@ def _setdefaults(self, defaults, kw): if kw.get(k, None) is None: kw[k] = defaults[k] - def _makeline(self, axes, x, y, kw, kwargs): + def _make_line(self, axes, x, y, kw, kwargs): kw = {**kw, **kwargs} # Don't modify the original kw. self._setdefaults(self._getdefaults(kw), kw) seg = mlines.Line2D(x, y, **kw) return seg, kw - def _makefill(self, axes, x, y, kw, kwargs): + def _make_coordinates(self, axes, x, y, kw, kwargs): + kw = {**kw, **kwargs} # Don't modify the original kw. + self._setdefaults(self._getdefaults(kw), kw) + return (x, y), kw + + def _make_polygon(self, axes, x, y, kw, kwargs): # Polygon doesn't directly support unitized inputs. x = axes.convert_xunits(x) y = axes.convert_yunits(y) @@ -493,11 +501,15 @@ def _plot_args(self, axes, tup, kwargs, *, if y.ndim == 1: y = y[:, np.newaxis] - if self.command == 'plot': - make_artist = self._makeline - else: + if self.output == 'Line2D': + make_artist = self._make_line + elif self.output == 'Polygon': kw['closed'] = kwargs.get('closed', True) - make_artist = self._makefill + make_artist = self._make_polygon + elif self.output == 'coordinates': + make_artist = self._make_coordinates + else: + _api.check_in_list(['Line2D', 'Polygon', 'coordinates'], output=self.output) ncx, ncy = x.shape[1], y.shape[1] if ncx > 1 and ncy > 1 and ncx != ncy: @@ -551,6 +563,9 @@ class _AxesBase(martist.Artist): _subclass_uses_cla = False + dataLim: mtransforms.Bbox + """The bounding `.Bbox` enclosing all data displayed in the Axes.""" + @property def _axis_map(self): """A mapping of axis names, e.g. 'x', to `Axis` instances.""" @@ -646,7 +661,7 @@ def __init__(self, fig, self._aspect = 'auto' self._adjustable = 'box' self._anchor = 'C' - self._stale_viewlims = {name: False for name in self._axis_names} + self._stale_viewlims = dict.fromkeys(self._axis_names, False) self._forward_navigation_events = forward_navigation_events self._sharex = sharex self._sharey = sharey @@ -777,8 +792,8 @@ def __repr__(self): if titles: fields += [f"title={titles}"] for name, axis in self._axis_map.items(): - if axis.get_label() and axis.get_label().get_text(): - fields += [f"{name}label={axis.get_label().get_text()!r}"] + if axis.label and axis.label.get_text(): + fields += [f"{name}label={axis.label.get_text()!r}"] return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">" def get_subplotspec(self): @@ -788,7 +803,7 @@ def get_subplotspec(self): def set_subplotspec(self, subplotspec): """Set the `.SubplotSpec`. associated with the subplot.""" self._subplotspec = subplotspec - self._set_position(subplotspec.get_position(self.figure)) + self._set_position(subplotspec.get_position(self.get_figure(root=False))) def get_gridspec(self): """Return the `.GridSpec` associated with the subplot, or None.""" @@ -849,6 +864,7 @@ def _unstale_viewLim(self): @property def viewLim(self): + """The view limits as `.Bbox` in data coordinates.""" self._unstale_viewLim() return self._viewLim @@ -959,8 +975,9 @@ def get_xaxis_text1_transform(self, pad_points): """ labels_align = mpl.rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick1') + - mtransforms.ScaledTranslation(0, -1 * pad_points / 72, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + 0, -1 * pad_points / 72, + self.get_figure(root=False).dpi_scale_trans), "top", labels_align) def get_xaxis_text2_transform(self, pad_points): @@ -985,8 +1002,9 @@ def get_xaxis_text2_transform(self, pad_points): """ labels_align = mpl.rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick2') + - mtransforms.ScaledTranslation(0, pad_points / 72, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + 0, pad_points / 72, + self.get_figure(root=False).dpi_scale_trans), "bottom", labels_align) def get_yaxis_transform(self, which='grid'): @@ -1039,8 +1057,9 @@ def get_yaxis_text1_transform(self, pad_points): """ labels_align = mpl.rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick1') + - mtransforms.ScaledTranslation(-1 * pad_points / 72, 0, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + -1 * pad_points / 72, 0, + self.get_figure(root=False).dpi_scale_trans), labels_align, "right") def get_yaxis_text2_transform(self, pad_points): @@ -1065,8 +1084,9 @@ def get_yaxis_text2_transform(self, pad_points): """ labels_align = mpl.rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick2') + - mtransforms.ScaledTranslation(pad_points / 72, 0, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + pad_points / 72, 0, + self.get_figure(root=False).dpi_scale_trans), labels_align, "left") def _update_transScale(self): @@ -1173,7 +1193,7 @@ def get_axes_locator(self): def _set_artist_props(self, a): """Set the boilerplate props for artists added to Axes.""" - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) if not a.is_transform_set(): a.set_transform(self.transData) @@ -1291,12 +1311,17 @@ def __clear(self): self._use_sticky_edges = True self._get_lines = _process_plot_var_args() - self._get_patches_for_fill = _process_plot_var_args('fill') + self._get_patches_for_fill = _process_plot_var_args('Polygon') self._gridOn = mpl.rcParams['axes.grid'] + # Swap children to minimize time we spend in an invalid state old_children, self._children = self._children, [] for chld in old_children: - chld.axes = chld.figure = None + chld._remove_method = None + chld._parent_figure = None + chld.axes = None + # Use list.clear to break the `artist._remove_method` reference cycle + old_children.clear() self._mouseover_set = _OrderedSet() self.child_axes = [] self._current_image = None # strictly for pyplot via _sci, _gci @@ -1347,7 +1372,7 @@ def __clear(self): # the other artists. We use the frame to draw the edges so we are # setting the edgecolor to None. self.patch = self._gen_axes_patch() - self.patch.set_figure(self.figure) + self.patch.set_figure(self.get_figure(root=False)) self.patch.set_facecolor(self._facecolor) self.patch.set_edgecolor('none') self.patch.set_linewidth(0) @@ -1522,7 +1547,7 @@ def _set_title_offset_trans(self, title_offset_points): """ self.titleOffsetTrans = mtransforms.ScaledTranslation( 0.0, title_offset_points / 72, - self.figure.dpi_scale_trans) + self.get_figure(root=False).dpi_scale_trans) for _title in (self.title, self._left_title, self._right_title): _title.set_transform(self.transAxes + self.titleOffsetTrans) _title.set_clip_box(None) @@ -1538,7 +1563,7 @@ def set_prop_cycle(self, *args, **kwargs): Call signatures:: set_prop_cycle(cycler) - set_prop_cycle(label=values[, label2=values2[, ...]]) + set_prop_cycle(label=values, label2=values2, ...) set_prop_cycle(label, values) Form 1 sets given `~cycler.Cycler` object. @@ -1705,7 +1730,8 @@ def set_adjustable(self, adjustable, share=False): ---------- adjustable : {'box', 'datalim'} If 'box', change the physical dimensions of the Axes. - If 'datalim', change the ``x`` or ``y`` data limits. + If 'datalim', change the ``x`` or ``y`` data limits. This + may ignore explicitly defined axis limits. share : bool, default: False If ``True``, apply the settings to all shared Axes. @@ -1937,7 +1963,7 @@ def apply_aspect(self, position=None): self._set_position(position, which='active') return - trans = self.get_figure().transSubfigure + trans = self.get_figure(root=False).transSubfigure bb = mtransforms.Bbox.unit().transformed(trans) # this is the physical aspect of the panel (or figure): fig_aspect = bb.height / bb.width @@ -2022,11 +2048,17 @@ def apply_aspect(self, position=None): yc = 0.5 * (ymin + ymax) y0 = yc - Ysize / 2.0 y1 = yc + Ysize / 2.0 + if not self.get_autoscaley_on(): + _log.warning("Ignoring fixed y limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_ybound(y_trf.inverted().transform([y0, y1])) else: xc = 0.5 * (xmin + xmax) x0 = xc - Xsize / 2.0 x1 = xc + Xsize / 2.0 + if not self.get_autoscalex_on(): + _log.warning("Ignoring fixed x limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_xbound(x_trf.inverted().transform([x0, x1])) def axis(self, arg=None, /, *, emit=True, **kwargs): @@ -2243,8 +2275,8 @@ def add_artist(self, a): Use `add_artist` only for artists for which there is no dedicated "add" method; and if necessary, use a method such as `update_datalim` - to manually update the dataLim if the artist is to be included in - autoscaling. + to manually update the `~.Axes.dataLim` if the artist is to be included + in autoscaling. If no ``transform`` has been specified when creating the artist (e.g. ``artist.get_transform() == None``) then the transform is set to @@ -2261,7 +2293,7 @@ def add_artist(self, a): def add_child_axes(self, ax): """ - Add an `.AxesBase` to the Axes' children; return the child Axes. + Add an `.Axes` to the Axes' children; return the child Axes. This is the lowlevel version. See `.axes.Axes.inset_axes`. """ @@ -2274,7 +2306,7 @@ def add_child_axes(self, ax): self.child_axes.append(ax) ax._remove_method = functools.partial( - self.figure._remove_axes, owners=[self.child_axes]) + self.get_figure(root=False)._remove_axes, owners=[self.child_axes]) self.stale = True return ax @@ -2357,7 +2389,7 @@ def _add_text(self, txt): def _update_line_limits(self, line): """ - Figures out the data limit of the given line, updating self.dataLim. + Figures out the data limit of the given line, updating `.Axes.dataLim`. """ path = line.get_path() if path.vertices.size == 0: @@ -2635,7 +2667,7 @@ def set_autoscale_on(self, b): @property def use_sticky_edges(self): """ - When autoscaling, whether to obey all `Artist.sticky_edges`. + When autoscaling, whether to obey all `.Artist.sticky_edges`. Default is ``True``. @@ -2732,19 +2764,22 @@ def set_ymargin(self, m): def margins(self, *margins, x=None, y=None, tight=True): """ - Set or retrieve autoscaling margins. + Set or retrieve margins around the data for autoscaling axis limits. + + This allows to configure the padding around the data without having to + set explicit limits using `~.Axes.set_xlim` / `~.Axes.set_ylim`. + + Autoscaling determines the axis limits by adding *margin* times the + data interval as padding around the data. See the following illustration: + + .. plot:: _embedded_plots/axes_margins.py - The padding added to each limit of the Axes is the *margin* - times the data interval. All input parameters must be floats - greater than -0.5. Passing both positional and keyword - arguments is invalid and will raise a TypeError. If no - arguments (positional or otherwise) are provided, the current + All input parameters must be floats greater than -0.5. Passing both + positional and keyword arguments is invalid and will raise a TypeError. + If no arguments (positional or otherwise) are provided, the current margins will remain unchanged and simply be returned. - Specifying any margin changes only the autoscaling; for example, - if *xmargin* is not None, then *xmargin* times the X data - interval will be added to each end of that interval before - it is used in autoscaling. + The default margins are :rc:`axes.xmargin` and :rc:`axes.ymargin`. Parameters ---------- @@ -2776,10 +2811,14 @@ def margins(self, *margins, x=None, y=None, tight=True): Notes ----- If a previously used Axes method such as :meth:`pcolor` has set - :attr:`use_sticky_edges` to `True`, only the limits not set by - the "sticky artists" will be modified. To force all of the - margins to be set, set :attr:`use_sticky_edges` to `False` + `~.Axes.use_sticky_edges` to `True`, only the limits not set by + the "sticky artists" will be modified. To force all + margins to be set, set `~.Axes.use_sticky_edges` to `False` before calling :meth:`margins`. + + See Also + -------- + .Axes.set_xmargin, .Axes.set_ymargin """ if margins and (x is not None or y is not None): @@ -3013,35 +3052,40 @@ def _update_title_position(self, renderer): titles = (self.title, self._left_title, self._right_title) + if not any(title.get_text() for title in titles): + # If the titles are all empty, there is no need to update their positions. + return + # Need to check all our twins too, aligned axes, and all the children # as well. axs = set() axs.update(self.child_axes) axs.update(self._twinned_axes.get_siblings(self)) - axs.update(self.figure._align_label_groups['title'].get_siblings(self)) + axs.update( + self.get_figure(root=False)._align_label_groups['title'].get_siblings(self)) for ax in self.child_axes: # Child positions must be updated first. locator = ax.get_axes_locator() ax.apply_aspect(locator(self, renderer) if locator else None) + top = -np.inf + for ax in axs: + bb = None + xticklabel_top = any(tick.label2.get_visible() for tick in + [ax.xaxis.majorTicks[0], ax.xaxis.minorTicks[0]]) + if (xticklabel_top or ax.xaxis.get_label_position() == 'top'): + bb = ax.xaxis.get_tightbbox(renderer) + if bb is None: + # Extent of the outline for colorbars, of the axes otherwise. + bb = ax.spines.get("outline", ax).get_window_extent() + top = max(top, bb.ymax) + for title in titles: x, _ = title.get_position() # need to start again in case of window resizing title.set_position((x, 1.0)) - top = -np.inf - for ax in axs: - bb = None - if (ax.xaxis.get_ticks_position() in ['top', 'unknown'] - or ax.xaxis.get_label_position() == 'top'): - bb = ax.xaxis.get_tightbbox(renderer) - if bb is None: - if 'outline' in ax.spines: - # Special case for colorbars: - bb = ax.spines['outline'].get_window_extent() - else: - bb = ax.get_window_extent(renderer) - top = max(top, bb.ymax) - if title.get_text(): + if title.get_text(): + for ax in axs: ax.yaxis.get_tightbbox(renderer) # update offsetText if ax.yaxis.offsetText.get_text(): bb = ax.yaxis.offsetText.get_tightbbox(renderer) @@ -3104,7 +3148,7 @@ def draw(self, renderer): for _axis in self._axis_map.values(): artists.remove(_axis) - if not self.figure.canvas.is_saving(): + 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)] @@ -3132,10 +3176,10 @@ def draw(self, renderer): artists = [self.patch] + artists if artists_rasterized: - _draw_rasterized(self.figure, artists_rasterized, renderer) + _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer) mimage._draw_list_compositing_images( - renderer, self, artists, self.figure.suppressComposite) + renderer, self, artists, self.get_figure(root=True).suppressComposite) renderer.close_group('axes') self.stale = False @@ -3144,7 +3188,7 @@ def draw_artist(self, a): """ Efficiently redraw a single artist. """ - a.draw(self.figure.canvas.get_renderer()) + a.draw(self.get_figure(root=True).canvas.get_renderer()) def redraw_in_frame(self): """ @@ -3154,7 +3198,7 @@ def redraw_in_frame(self): for artist in [*self._axis_map.values(), self.title, self._left_title, self._right_title]: stack.enter_context(artist._cm_set(visible=False)) - self.draw(self.figure.canvas.get_renderer()) + self.draw(self.get_figure(root=True).canvas.get_renderer()) # Axes rectangle characteristics @@ -3226,7 +3270,7 @@ def set_axisbelow(self, b): axis.set_zorder(zorder) self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def grid(self, visible=None, which='major', axis='both', **kwargs): """ Configure the grid lines. @@ -3501,7 +3545,7 @@ def get_xlabel(self): """ Get the xlabel text string. """ - label = self.xaxis.get_label() + label = self.xaxis.label return label.get_text() def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, @@ -3754,7 +3798,7 @@ def get_ylabel(self): """ Get the ylabel text string. """ - label = self.yaxis.get_label() + label = self.yaxis.label return label.get_text() def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, @@ -4416,9 +4460,8 @@ def get_default_bbox_extra_artists(self): return [a for a in artists if a.get_visible() and a.get_in_layout() and (isinstance(a, noclip) or not a._fully_clipped_to_axes())] - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, - bbox_extra_artists=None, *, for_layout_only=False): + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, + bbox_extra_artists=None, for_layout_only=False): """ Return the tight bounding box of the Axes, including axis and their decorators (xlabel, title, etc). @@ -4462,7 +4505,7 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True, bb = [] if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): return None @@ -4513,9 +4556,9 @@ def _make_twin_axes(self, *args, **kwargs): raise ValueError("Twinned Axes may share only one axis") ss = self.get_subplotspec() if ss: - twin = self.figure.add_subplot(ss, *args, **kwargs) + twin = self.get_figure(root=False).add_subplot(ss, *args, **kwargs) else: - twin = self.figure.add_axes( + twin = self.get_figure(root=False).add_axes( self.get_position(True), *args, **kwargs, axes_locator=_TransformedBoundsLocator( [0, 0, 1, 1], self.transAxes)) @@ -4744,6 +4787,12 @@ def __init__(self, figure, artists): self.figure = figure self.artists = artists + def get_figure(self, root=False): + if root: + return self.figure.get_figure(root=True) + else: + return self.figure + @martist.allow_rasterization def draw(self, renderer): for a in self.artists: diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 8cd88a92cc09..ee3c7cf0dee9 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -10,11 +10,11 @@ from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent from matplotlib.cbook import CallbackRegistry from matplotlib.container import Container from matplotlib.collections import Collection -from matplotlib.cm import ScalarMappable +from matplotlib.colorizer import ColorizingArtist from matplotlib.legend import Legend from matplotlib.lines import Line2D from matplotlib.gridspec import SubplotSpec, GridSpec -from matplotlib.figure import Figure +from matplotlib.figure import Figure, SubFigure from matplotlib.image import AxesImage from matplotlib.patches import Patch from matplotlib.scale import ScaleBase @@ -82,7 +82,7 @@ class _AxesBase(martist.Artist): def get_subplotspec(self) -> SubplotSpec | None: ... def set_subplotspec(self, subplotspec: SubplotSpec) -> None: ... def get_gridspec(self) -> GridSpec | None: ... - def set_figure(self, fig: Figure) -> None: ... + def set_figure(self, fig: Figure | SubFigure) -> None: ... @property def viewLim(self) -> Bbox: ... def get_xaxis_transform( @@ -400,7 +400,7 @@ class _AxesBase(martist.Artist): def get_xticklines(self, minor: bool = ...) -> list[Line2D]: ... def get_ygridlines(self) -> list[Line2D]: ... def get_yticklines(self, minor: bool = ...) -> list[Line2D]: ... - def _sci(self, im: ScalarMappable) -> None: ... + def _sci(self, im: ColorizingArtist) -> None: ... def get_autoscalex_on(self) -> bool: ... def get_autoscaley_on(self) -> bool: ... def set_autoscalex_on(self, b: bool) -> None: ... diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 3fabf49ebb38..15a1970fa4a6 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -27,13 +27,14 @@ def __init__(self, parent, orientation, location, functions, transform=None, self._orientation = orientation self._ticks_set = False + fig = self._parent.get_figure(root=False) if self._orientation == 'x': - super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs) + super().__init__(fig, [0, 1., 1, 0.0001], **kwargs) self._axis = self.xaxis self._locstrings = ['top', 'bottom'] self._otherstrings = ['left', 'right'] else: # 'y' - super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs) + super().__init__(fig, [0, 1., 0.0001, 1], **kwargs) self._axis = self.yaxis self._locstrings = ['right', 'left'] self._otherstrings = ['top', 'bottom'] @@ -318,4 +319,4 @@ def set_color(self, color): **kwargs : `~matplotlib.axes.Axes` properties. Other miscellaneous Axes parameters. ''' -_docstring.interpd.update(_secax_docstring=_secax_docstring) +_docstring.interpd.register(_secax_docstring=_secax_docstring) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 98f7db89b09f..56eeb0e4169b 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -29,7 +29,7 @@ # allows all Line2D kwargs. _line_inspector = martist.ArtistInspector(mlines.Line2D) _line_param_names = _line_inspector.get_setters() -_line_param_aliases = [list(d)[0] for d in _line_inspector.aliasd.values()] +_line_param_aliases = [next(iter(d)) for d in _line_inspector.aliasd.values()] _gridline_param_names = ['grid_' + name for name in _line_param_names + _line_param_aliases] @@ -96,7 +96,7 @@ def __init__( else: gridOn = False - self.set_figure(axes.figure) + self.set_figure(axes.get_figure(root=False)) self.axes = axes self._loc = loc @@ -231,7 +231,6 @@ def get_children(self): self.gridline, self.label1, self.label2] return children - @_api.rename_parameter("3.8", "clippath", "path") def set_clip_path(self, path, transform=None): # docstring inherited super().set_clip_path(path, transform) @@ -278,32 +277,6 @@ def draw(self, renderer): renderer.close_group(self.__name__) self.stale = False - @_api.deprecated("3.8") - def set_label1(self, s): - """ - Set the label1 text. - - Parameters - ---------- - s : str - """ - self.label1.set_text(s) - self.stale = True - - set_label = set_label1 - - @_api.deprecated("3.8") - def set_label2(self, s): - """ - Set the label2 text. - - Parameters - ---------- - s : str - """ - self.label2.set_text(s) - self.stale = True - def set_url(self, url): """ Set the url of label1 and label2. @@ -318,7 +291,7 @@ def set_url(self, url): self.stale = True def _set_artist_props(self, a): - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) def get_view_interval(self): """ @@ -565,17 +538,19 @@ def __get__(self, instance, owner): # instance._get_tick() can itself try to access the majorTicks # attribute (e.g. in certain projection classes which override # e.g. get_xaxis_text1_transform). In order to avoid infinite - # recursion, first set the majorTicks on the instance to an empty - # list, then create the tick and append it. + # recursion, first set the majorTicks on the instance temporarily + # to an empty lis. Then create the tick; note that _get_tick() + # may call reset_ticks(). Therefore, the final tick list is + # created and assigned afterwards. if self._major: instance.majorTicks = [] tick = instance._get_tick(major=True) - instance.majorTicks.append(tick) + instance.majorTicks = [tick] return instance.majorTicks else: instance.minorTicks = [] tick = instance._get_tick(major=False) - instance.minorTicks.append(tick) + instance.minorTicks = [tick] return instance.minorTicks @@ -599,7 +574,7 @@ class Axis(martist.Artist): The axis label. labelpad : float The distance between the axis label and the tick labels. - Defaults to :rc:`axes.labelpad` = 4. + Defaults to :rc:`axes.labelpad`. offsetText : `~matplotlib.text.Text` A `.Text` object containing the data offset of the ticks (if any). pickradius : float @@ -625,6 +600,10 @@ class Axis(martist.Artist): # The class used in _get_tick() to create tick instances. Must either be # overwritten in subclasses, or subclasses must reimplement _get_tick(). _tick_class = None + converter = _api.deprecate_privatize_attribute( + "3.10", + alternative="get_converter and set_converter methods" + ) def __str__(self): return "{}({},{})".format( @@ -648,7 +627,7 @@ def __init__(self, axes, *, pickradius=15, clear=True): super().__init__() self._remove_overlapping_locs = True - self.set_figure(axes.figure) + self.set_figure(axes.get_figure(root=False)) self.isDefault_label = True @@ -664,7 +643,8 @@ def __init__(self, axes, *, pickradius=15, clear=True): fontsize=mpl.rcParams['axes.labelsize'], fontweight=mpl.rcParams['axes.labelweight'], color=mpl.rcParams['axes.labelcolor'], - ) + ) #: The `.Text` object of the axis label. + self._set_artist_props(self.label) self.offsetText = mtext.Text(np.nan, np.nan) self._set_artist_props(self.offsetText) @@ -680,7 +660,8 @@ def __init__(self, axes, *, pickradius=15, clear=True): if clear: self.clear() else: - self.converter = None + self._converter = None + self._converter_is_explicit = False self.units = None self._autoscale_on = True @@ -729,8 +710,8 @@ def _get_shared_axis(self): def _get_axis_name(self): """Return the axis name.""" - return [name for name, axis in self.axes._axis_map.items() - if axis is self][0] + return next(name for name, axis in self.axes._axis_map.items() + if axis is self) # During initialization, Axis objects often create ticks that are later # unused; this turns out to be a very slow step. Instead, use a custom @@ -833,6 +814,10 @@ def _set_axes_scale(self, value, **kwargs): **{f"scale{k}": k == name for k in self.axes._axis_names}) def limit_range_for_scale(self, vmin, vmax): + """ + Return the range *vmin*, *vmax*, restricted to the domain supported by the + current scale. + """ return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) def _get_autoscale_on(self): @@ -841,8 +826,9 @@ def _get_autoscale_on(self): def _set_autoscale_on(self, b): """ - Set whether this Axis is autoscaled when drawing or by - `.Axes.autoscale_view`. If b is None, then the value is not changed. + Set whether this Axis is autoscaled when drawing or by `.Axes.autoscale_view`. + + If b is None, then the value is not changed. Parameters ---------- @@ -905,7 +891,8 @@ def clear(self): mpl.rcParams['axes.grid.which'] in ('both', 'minor')) self.reset_ticks() - self.converter = None + self._converter = None + self._converter_is_explicit = False self.units = None self.stale = True @@ -1066,8 +1053,8 @@ def get_tick_params(self, which='major'): ) return self._translate_tick_params(self._minor_tick_kw, reverse=True) - @staticmethod - def _translate_tick_params(kw, reverse=False): + @classmethod + def _translate_tick_params(cls, kw, reverse=False): """ Translate the kwargs supported by `.Axis.set_tick_params` to kwargs supported by `.Tick._apply_params`. @@ -1109,10 +1096,15 @@ def _translate_tick_params(kw, reverse=False): 'labeltop': 'label2On', } if reverse: - kwtrans = { - oldkey: kw_.pop(newkey) - for oldkey, newkey in keymap.items() if newkey in kw_ - } + kwtrans = {} + is_x_axis = cls.axis_name == 'x' + y_axis_keys = ['left', 'right', 'labelleft', 'labelright'] + for oldkey, newkey in keymap.items(): + if newkey in kw_: + if is_x_axis and oldkey in y_axis_keys: + continue + else: + kwtrans[oldkey] = kw_.pop(newkey) else: kwtrans = { newkey: kw_.pop(oldkey) @@ -1131,7 +1123,6 @@ def _translate_tick_params(kw, reverse=False): kwtrans.update(kw_) return kwtrans - @_api.rename_parameter("3.8", "clippath", "path") def set_clip_path(self, path, transform=None): super().set_clip_path(path, transform) for child in self.majorTicks + self.minorTicks: @@ -1281,8 +1272,9 @@ def _set_lim(self, v0, v1, *, emit=True, auto): other._axis_map[name]._set_lim(v0, v1, emit=False, auto=auto) if emit: other.callbacks.process(f"{name}lim_changed", other) - if other.figure != self.figure: - other.figure.canvas.draw_idle() + if ((other_fig := other.get_figure(root=False)) != + self.get_figure(root=False)): + other_fig.canvas.draw_idle() self.stale = True return v0, v1 @@ -1290,7 +1282,7 @@ def _set_lim(self, v0, v1, *, emit=True, auto): def _set_artist_props(self, a): if a is None: return - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) def _update_ticks(self): """ @@ -1347,7 +1339,7 @@ def _update_ticks(self): def _get_ticklabel_bboxes(self, ticks, renderer=None): """Return lists of bboxes for ticks' label1's and label2's.""" if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() return ([tick.label1.get_window_extent(renderer) for tick in ticks if tick.label1.get_visible()], [tick.label2.get_window_extent(renderer) @@ -1363,10 +1355,10 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): collapsed to near zero. This allows tight/constrained_layout to ignore too-long labels when doing their layout. """ - if not self.get_visible(): + if not self.get_visible() or for_layout_only and not self.get_in_layout(): return if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() ticks_to_draw = self._update_ticks() self._update_label_position(renderer) @@ -1443,8 +1435,21 @@ def get_gridlines(self): return cbook.silent_list('Line2D gridline', [tick.gridline for tick in ticks]) + def set_label(self, s): + """Assigning legend labels is not supported. Raises RuntimeError.""" + raise RuntimeError( + "A legend label cannot be assigned to an Axis. Did you mean to " + "set the axis label via set_label_text()?") + def get_label(self): - """Return the axis label as a Text instance.""" + """ + Return the axis label as a Text instance. + + .. admonition:: Discouraged + + This overrides `.Artist.get_label`, which is for legend labels, with a new + semantic. It is recommended to use the attribute ``Axis.label`` instead. + """ return self.label def get_offset_text(self): @@ -1744,16 +1749,20 @@ def grid(self, visible=None, which='major', **kwargs): def update_units(self, data): """ Introspect *data* for units converter and update the - ``axis.converter`` instance if necessary. Return *True* + ``axis.get_converter`` instance if necessary. Return *True* if *data* is registered for unit conversion. """ - converter = munits.registry.get_converter(data) + if not self._converter_is_explicit: + converter = munits.registry.get_converter(data) + else: + converter = self._converter + if converter is None: return False - neednew = self.converter != converter - self.converter = converter - default = self.converter.default_units(data, self) + neednew = self._converter != converter + self._set_converter(converter) + default = self._converter.default_units(data, self) if default is not None and self.units is None: self.set_units(default) @@ -1767,10 +1776,10 @@ def _update_axisinfo(self): Check the axis converter for the stored units to see if the axis info needs to be updated. """ - if self.converter is None: + if self._converter is None: return - info = self.converter.axisinfo(self.units, self) + info = self._converter.axisinfo(self.units, self) if info is None: return @@ -1797,25 +1806,62 @@ def _update_axisinfo(self): self.set_default_intervals() def have_units(self): - return self.converter is not None or self.units is not None + return self._converter is not None or self.units is not None def convert_units(self, x): # If x is natively supported by Matplotlib, doesn't need converting if munits._is_natively_supported(x): return x - if self.converter is None: - self.converter = munits.registry.get_converter(x) + if self._converter is None: + self._set_converter(munits.registry.get_converter(x)) - if self.converter is None: + if self._converter is None: return x try: - ret = self.converter.convert(x, self.units, self) + ret = self._converter.convert(x, self.units, self) except Exception as e: raise munits.ConversionError('Failed to convert value(s) to axis ' f'units: {x!r}') from e return ret + def get_converter(self): + """ + Get the unit converter for axis. + + Returns + ------- + `~matplotlib.units.ConversionInterface` or None + """ + return self._converter + + def set_converter(self, converter): + """ + Set the unit converter for axis. + + Parameters + ---------- + converter : `~matplotlib.units.ConversionInterface` + """ + self._set_converter(converter) + self._converter_is_explicit = True + + def _set_converter(self, converter): + if self._converter is converter or self._converter == converter: + return + if self._converter_is_explicit: + raise RuntimeError("Axis already has an explicit converter set") + elif ( + self._converter is not None and + not isinstance(converter, type(self._converter)) and + not isinstance(self._converter, type(converter)) + ): + _api.warn_external( + "This axis already has a converter set and " + "is updating to a potentially incompatible converter" + ) + self._converter = converter + def set_units(self, u): """ Set the units for axis. @@ -2194,9 +2240,9 @@ def _get_tick_boxes_siblings(self, renderer): """ # Get the Grouper keeping track of x or y label groups for this figure. name = self._get_axis_name() - if name not in self.figure._align_label_groups: + if name not in self.get_figure(root=False)._align_label_groups: return [], [] - grouper = self.figure._align_label_groups[name] + grouper = self.get_figure(root=False)._align_label_groups[name] bboxes = [] bboxes2 = [] # If we want to align labels from other Axes: @@ -2411,34 +2457,20 @@ def _update_label_position(self, renderer): # get bounding boxes for this axis and any siblings # that have been set by `fig.align_xlabels()` bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer) - x, y = self.label.get_position() - if self.label_position == 'bottom': - try: - spine = self.axes.spines['bottom'] - spinebbox = spine.get_window_extent() - except KeyError: - # use Axes if spine doesn't exist - spinebbox = self.axes.bbox - bbox = mtransforms.Bbox.union(bboxes + [spinebbox]) - bottom = bbox.y0 + if self.label_position == 'bottom': + # Union with extents of the bottom spine if present, of the axes otherwise. + bbox = mtransforms.Bbox.union([ + *bboxes, self.axes.spines.get("bottom", self.axes).get_window_extent()]) self.label.set_position( - (x, bottom - self.labelpad * self.figure.dpi / 72) - ) + (x, bbox.y0 - self.labelpad * self.get_figure(root=True).dpi / 72)) else: - try: - spine = self.axes.spines['top'] - spinebbox = spine.get_window_extent() - except KeyError: - # use Axes if spine doesn't exist - spinebbox = self.axes.bbox - bbox = mtransforms.Bbox.union(bboxes2 + [spinebbox]) - top = bbox.y1 - + # Union with extents of the top spine if present, of the axes otherwise. + bbox = mtransforms.Bbox.union([ + *bboxes2, self.axes.spines.get("top", self.axes).get_window_extent()]) self.label.set_position( - (x, top + self.labelpad * self.figure.dpi / 72) - ) + (x, bbox.y1 + self.labelpad * self.get_figure(root=True).dpi / 72)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2454,14 +2486,14 @@ def _update_offset_text_position(self, bboxes, bboxes2): else: bbox = mtransforms.Bbox.union(bboxes) bottom = bbox.y0 - y = bottom - self.OFFSETTEXTPAD * self.figure.dpi / 72 + y = bottom - self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72 else: if not len(bboxes2): top = self.axes.bbox.ymax else: bbox = mtransforms.Bbox.union(bboxes2) top = bbox.y1 - y = top + self.OFFSETTEXTPAD * self.figure.dpi / 72 + y = top + self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72 self.offsetText.set_position((x, y)) def set_ticks_position(self, position): @@ -2549,8 +2581,8 @@ def set_default_intervals(self): # not changed the view: if (not self.axes.dataLim.mutatedx() and not self.axes.viewLim.mutatedx()): - if self.converter is not None: - info = self.converter.axisinfo(self.units, self) + if self._converter is not None: + info = self._converter.axisinfo(self.units, self) if info.default_limits is not None: xmin, xmax = self.convert_units(info.default_limits) self.axes.viewLim.intervalx = xmin, xmax @@ -2558,7 +2590,7 @@ def set_default_intervals(self): def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( - self.axes.transAxes - self.figure.dpi_scale_trans) + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.width * 72 # There is a heuristic here that the aspect ratio of tick text # is no more than 3:1 @@ -2653,32 +2685,19 @@ def _update_label_position(self, renderer): # that have been set by `fig.align_ylabels()` bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer) x, y = self.label.get_position() + if self.label_position == 'left': - try: - spine = self.axes.spines['left'] - spinebbox = spine.get_window_extent() - except KeyError: - # use Axes if spine doesn't exist - spinebbox = self.axes.bbox - bbox = mtransforms.Bbox.union(bboxes + [spinebbox]) - left = bbox.x0 + # Union with extents of the left spine if present, of the axes otherwise. + bbox = mtransforms.Bbox.union([ + *bboxes, self.axes.spines.get("left", self.axes).get_window_extent()]) self.label.set_position( - (left - self.labelpad * self.figure.dpi / 72, y) - ) - + (bbox.x0 - self.labelpad * self.get_figure(root=True).dpi / 72, y)) else: - try: - spine = self.axes.spines['right'] - spinebbox = spine.get_window_extent() - except KeyError: - # use Axes if spine doesn't exist - spinebbox = self.axes.bbox - - bbox = mtransforms.Bbox.union(bboxes2 + [spinebbox]) - right = bbox.x1 + # Union with extents of the right spine if present, of the axes otherwise. + bbox = mtransforms.Bbox.union([ + *bboxes2, self.axes.spines.get("right", self.axes).get_window_extent()]) self.label.set_position( - (right + self.labelpad * self.figure.dpi / 72, y) - ) + (bbox.x1 + self.labelpad * self.get_figure(root=True).dpi / 72, y)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2693,7 +2712,7 @@ def _update_offset_text_position(self, bboxes, bboxes2): bbox = self.axes.bbox top = bbox.ymax self.offsetText.set_position( - (x, top + self.OFFSETTEXTPAD * self.figure.dpi / 72) + (x, top + self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72) ) def set_offset_position(self, position): @@ -2792,8 +2811,8 @@ def set_default_intervals(self): # not changed the view: if (not self.axes.dataLim.mutatedy() and not self.axes.viewLim.mutatedy()): - if self.converter is not None: - info = self.converter.axisinfo(self.units, self) + if self._converter is not None: + info = self._converter.axisinfo(self.units, self) if info.default_limits is not None: ymin, ymax = self.convert_units(info.default_limits) self.axes.viewLim.intervaly = ymin, ymax @@ -2801,7 +2820,7 @@ def set_default_intervals(self): def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( - self.axes.transAxes - self.figure.dpi_scale_trans) + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.height * 72 # Having a spacing of at least 2 just looks good. size = self._get_tick_label_size('y') * 2 diff --git a/lib/matplotlib/axis.pyi b/lib/matplotlib/axis.pyi index e23ae381c338..f2c5b1fc586d 100644 --- a/lib/matplotlib/axis.pyi +++ b/lib/matplotlib/axis.pyi @@ -1,6 +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 import numpy as np from numpy.typing import ArrayLike @@ -14,6 +15,7 @@ from matplotlib.text import Text from matplotlib.ticker import Locator, Formatter from matplotlib.transforms import Transform, Bbox from matplotlib.typing import ColorType +from matplotlib.units import ConversionInterface GRIDLINE_INTERPOLATION_STEPS: int @@ -59,9 +61,6 @@ class Tick(martist.Artist): def set_pad(self, val: float) -> None: ... def get_pad(self) -> None: ... def get_loc(self) -> float: ... - def set_label1(self, s: object) -> None: ... - def set_label(self, s: object) -> None: ... - def set_label2(self, s: object) -> None: ... def set_url(self, url: str | None) -> None: ... def get_view_interval(self) -> ArrayLike: ... def update_position(self, loc: float) -> None: ... @@ -93,9 +92,8 @@ class Ticker: class _LazyTickList: def __init__(self, major: bool) -> None: ... - # Replace return with Self when py3.9 is dropped @overload - def __get__(self, instance: None, owner: None) -> _LazyTickList: ... + def __get__(self, instance: None, owner: None) -> Self: ... @overload def __get__(self, instance: Axis, owner: type[Axis]) -> list[Tick]: ... @@ -210,6 +208,8 @@ class Axis(martist.Artist): def update_units(self, data): ... def have_units(self) -> bool: ... def convert_units(self, x): ... + def get_converter(self) -> ConversionInterface | None: ... + def set_converter(self, converter: ConversionInterface) -> None: ... def set_units(self, u) -> None: ... def get_units(self): ... def set_label_text( diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 53e5f6b23213..d39fc0a1288b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -510,75 +510,44 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): angle : float The rotation angle in degrees anti-clockwise. ismath : bool or "TeX" - If True, use mathtext parser. If "TeX", use tex for rendering. + If True, use mathtext parser. mtext : `~matplotlib.text.Text` The original text object to be rendered. + + Notes + ----- + **Notes for backend implementers:** + + `.RendererBase.draw_text` also supports passing "TeX" to the *ismath* + parameter to use TeX rendering, but this is not required for actual + rendering backends, and indeed many builtin backends do not support + this. Rather, TeX rendering is provided by `~.RendererBase.draw_tex`. """ self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) - def _get_text_path_transform(self, x, y, s, prop, angle, ismath): + def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ - Return the text path and transform. + Draw the text by converting them to paths using `.TextToPath`. - Parameters - ---------- - x : float - The x location of the text in display coords. - y : float - The y location of the text baseline in display coords. - s : str - The text to be converted. - prop : `~matplotlib.font_manager.FontProperties` - The font property. - angle : float - Angle in degrees to render the text at. - ismath : bool or "TeX" - If True, use mathtext parser. If "TeX", use tex for rendering. + This private helper supports the same parameters as + `~.RendererBase.draw_text`; setting *ismath* to "TeX" triggers TeX + rendering. """ - text2path = self._text2path fontsize = self.points_to_pixels(prop.get_size_in_points()) verts, codes = text2path.get_text_path(prop, s, ismath=ismath) - path = Path(verts, codes) - angle = np.deg2rad(angle) if self.flipy(): width, height = self.get_canvas_width_height() transform = (Affine2D() .scale(fontsize / text2path.FONT_SCALE) - .rotate(angle) + .rotate_deg(angle) .translate(x, height - y)) else: transform = (Affine2D() .scale(fontsize / text2path.FONT_SCALE) - .rotate(angle) + .rotate_deg(angle) .translate(x, y)) - - return path, transform - - def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): - """ - Draw the text by converting them to paths using `.TextToPath`. - - Parameters - ---------- - gc : `.GraphicsContextBase` - The graphics context. - x : float - The x location of the text in display coords. - y : float - The y location of the text baseline in display coords. - s : str - The text to be converted. - prop : `~matplotlib.font_manager.FontProperties` - The font property. - angle : float - Angle in degrees to render the text at. - ismath : bool or "TeX" - If True, use mathtext parser. If "TeX", use tex for rendering. - """ - path, transform = self._get_text_path_transform( - x, y, s, prop, angle, ismath) color = gc.get_rgb() gc.set_linewidth(0.0) self.draw_path(gc, path, transform, rgbFace=color) @@ -992,6 +961,10 @@ def get_hatch_linewidth(self): """Get the hatch linewidth.""" return self._hatch_linewidth + def set_hatch_linewidth(self, hatch_linewidth): + """Set the hatch linewidth.""" + self._hatch_linewidth = hatch_linewidth + def get_sketch_params(self): """ Return the sketch parameters for the artist. @@ -1209,26 +1182,12 @@ class Event: def __init__(self, name, canvas, guiEvent=None): self.name = name self.canvas = canvas - self._guiEvent = guiEvent - self._guiEvent_deleted = False + self.guiEvent = guiEvent def _process(self): """Process this event on ``self.canvas``, then unset ``guiEvent``.""" self.canvas.callbacks.process(self.name, self) - self._guiEvent_deleted = True - - @property - def guiEvent(self): - # After deprecation elapses: remove _guiEvent_deleted; make guiEvent a plain - # attribute set to None by _process. - if self._guiEvent_deleted: - _api.warn_deprecated( - "3.8", message="Accessing guiEvent outside of the original GUI event " - "handler is unsafe and deprecated since %(since)s; in the future, the " - "attribute will be set to None after quitting the event handler. You " - "may separately record the value of the guiEvent attribute at your own " - "risk.") - return self._guiEvent + self.guiEvent = None class DrawEvent(Event): @@ -1302,10 +1261,6 @@ class LocationEvent(Event): The keyboard modifiers currently being pressed (except for KeyEvent). """ - # Fully delete all occurrences of lastevent after deprecation elapses. - _lastevent = None - lastevent = _api.deprecated("3.8")( - _api.classproperty(lambda cls: cls._lastevent)) _last_axes_ref = None def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): @@ -1373,6 +1328,28 @@ class MouseEvent(LocationEvent): If this is unset, *name* is "scroll_event", and *step* is nonzero, then this will be set to "up" or "down" depending on the sign of *step*. + buttons : None or frozenset + For 'motion_notify_event', the mouse buttons currently being pressed + (a set of zero or more MouseButtons); + for other events, None. + + .. note:: + For 'motion_notify_event', this attribute is more accurate than + the ``button`` (singular) attribute, which is obtained from the last + 'button_press_event' or 'button_release_event' that occurred within + the canvas (and thus 1. be wrong if the last change in mouse state + occurred when the canvas did not have focus, and 2. cannot report + when multiple buttons are pressed). + + This attribute is not set for 'button_press_event' and + 'button_release_event' because GUI toolkits are inconsistent as to + whether they report the button state *before* or *after* the + press/release occurred. + + .. warning:: + On macOS, the Tk backends only report a single button even if + multiple buttons are pressed. + key : None or str The key pressed when the mouse event triggered, e.g. 'shift'. See `KeyEvent`. @@ -1405,7 +1382,8 @@ def on_press(event): """ def __init__(self, name, canvas, x, y, button=None, key=None, - step=0, dblclick=False, guiEvent=None, *, modifiers=None): + step=0, dblclick=False, guiEvent=None, *, + buttons=None, modifiers=None): super().__init__( name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers) if button in MouseButton.__members__.values(): @@ -1416,6 +1394,16 @@ def __init__(self, name, canvas, x, y, button=None, key=None, elif step < 0: button = "down" self.button = button + if name == "motion_notify_event": + self.buttons = frozenset(buttons if buttons is not None else []) + else: + # We don't support 'buttons' for button_press/release_event because + # toolkits are inconsistent as to whether they report the state + # before or after the event. + if buttons: + raise ValueError( + "'buttons' is only supported for 'motion_notify_event'") + self.buttons = None self.key = key self.step = step self.dblclick = dblclick @@ -1545,21 +1533,19 @@ def _mouse_handler(event): # done with the internal _set_inaxes method which ensures that # the xdata and ydata attributes are also correct. try: + canvas = last_axes.get_figure(root=True).canvas leave_event = LocationEvent( - "axes_leave_event", last_axes.figure.canvas, + "axes_leave_event", canvas, event.x, event.y, event.guiEvent, modifiers=event.modifiers) leave_event._set_inaxes(last_axes) - last_axes.figure.canvas.callbacks.process( - "axes_leave_event", leave_event) + canvas.callbacks.process("axes_leave_event", leave_event) except Exception: pass # The last canvas may already have been torn down. if event.inaxes is not None: event.canvas.callbacks.process("axes_enter_event", event) LocationEvent._last_axes_ref = ( weakref.ref(event.inaxes) if event.inaxes else None) - LocationEvent._lastevent = ( - None if event.name == "figure_leave_event" else event) def _get_renderer(figure, print_method=None): @@ -2003,8 +1989,8 @@ def _switch_canvas_and_return_print_method(self, fmt, backend=None): """ Context manager temporarily setting the canvas for saving the figure:: - with canvas._switch_canvas_and_return_print_method(fmt, backend) \\ - as print_method: + with (canvas._switch_canvas_and_return_print_method(fmt, backend) + as print_method): # ``print_method`` is a suitable ``print_{fmt}`` method, and # the figure's canvas is temporarily switched to the method's # canvas within the with... block. ``print_method`` is also @@ -2134,20 +2120,14 @@ def print_figure( if dpi == 'figure': dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) - if kwargs.get("papertype") == 'auto': - # When deprecation elapses, remove backend_ps._get_papertype & its callers. - _api.warn_deprecated( - "3.8", name="papertype='auto'", addendum="Pass an explicit paper type, " - "'figure', or omit the *papertype* argument entirely.") - # Remove the figure manager, if any, to avoid resizing the GUI widget. - with cbook._setattr_cm(self, manager=None), \ - self._switch_canvas_and_return_print_method(format, backend) \ - as print_method, \ - cbook._setattr_cm(self.figure, dpi=dpi), \ - cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), \ - cbook._setattr_cm(self.figure.canvas, _is_saving=True), \ - ExitStack() as stack: + with (cbook._setattr_cm(self, manager=None), + self._switch_canvas_and_return_print_method(format, backend) + as print_method, + cbook._setattr_cm(self.figure, dpi=dpi), + cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), + cbook._setattr_cm(self.figure.canvas, _is_saving=True), + ExitStack() as stack): for prop in ["facecolor", "edgecolor"]: color = locals()[prop] @@ -2227,29 +2207,22 @@ def get_default_filetype(cls): def get_default_filename(self): """ - Return a string, which includes extension, suitable for use as - a default filename. - """ - basename = (self.manager.get_window_title() if self.manager is not None - else '') - basename = (basename or 'image').replace(' ', '_') - filetype = self.get_default_filetype() - filename = basename + '.' + filetype - return filename - - @_api.deprecated("3.8") - def switch_backends(self, FigureCanvasClass): - """ - Instantiate an instance of FigureCanvasClass - - This is used for backend switching, e.g., to instantiate a - FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is - not done, so any changes to one of the instances (e.g., setting - figure size or line props), will be reflected in the other - """ - newCanvas = FigureCanvasClass(self.figure) - newCanvas._is_saving = self._is_saving - return newCanvas + Return a suitable default filename, including the extension. + """ + default_basename = ( + self.manager.get_window_title() + if self.manager is not None + else '' + ) + default_basename = default_basename or 'image' + # Characters to be avoided in a NT path: + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions + # plus ' ' + removed_chars = r'<>:"/\|?*\0 ' + default_basename = default_basename.translate( + {ord(c): "_" for c in removed_chars}) + default_filetype = self.get_default_filetype() + return f'{default_basename}.{default_filetype}' def mpl_connect(self, s, func): """ @@ -2520,27 +2493,27 @@ def _get_uniform_gridstate(ticks): scale = ax.get_yscale() if scale == 'log': ax.set_yscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() elif scale == 'linear': try: ax.set_yscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_yscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() # toggle scaling of x-axes between 'log and 'linear' (default key 'k') elif event.key in rcParams['keymap.xscale']: scalex = ax.get_xscale() if scalex == 'log': ax.set_xscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() elif scalex == 'linear': try: ax.set_xscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_xscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() def button_press_handler(event, canvas=None, toolbar=None): @@ -2844,6 +2817,8 @@ class NavigationToolbar2: ('Save', 'Save the figure', 'filesave', 'save_figure'), ) + UNKNOWN_SAVED_STATUS = object() + def __init__(self, canvas): self.canvas = canvas canvas.toolbar = self @@ -3258,7 +3233,26 @@ def on_tool_fig_close(e): return self.subplot_tool def save_figure(self, *args): - """Save the current figure.""" + """ + Save the current figure. + + Backend implementations may choose to return + the absolute path of the saved file, if any, as + a string. + + If no file is created then `None` is returned. + + If the backend does not implement this functionality + then `NavigationToolbar2.UNKNOWN_SAVED_STATUS` is returned. + + Returns + ------- + str or `NavigationToolbar2.UNKNOWN_SAVED_STATUS` or `None` + The filepath of the saved figure. + Returns `None` if figure is not saved. + Returns `NavigationToolbar2.UNKNOWN_SAVED_STATUS` when + the backend does not provide the information. + """ raise NotImplementedError def update(self): @@ -3363,7 +3357,7 @@ def _get_image_filename(self, tool): _api.warn_deprecated( "3.9", message=f"Loading icon {tool.image!r} from the current " "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed %(removal)s; " + "is deprecated since %(since)s and will be removed in %(removal)s; " "Tool.image should be set to a path relative to the Tool's source " "file, or to an absolute path.") return os.path.abspath(fname) @@ -3443,7 +3437,7 @@ def remove_toolitem(self, name): This hook must be implemented in each backend and contains the backend-specific code to remove an element from the toolbar; it is - called when `.ToolManager` emits a `tool_removed_event`. + called when `.ToolManager` emits a ``tool_removed_event``. Because some tools are present only on the `.ToolManager` but not on the `ToolContainer`, this method must be a no-op when called on a tool diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 075d87a6edd8..8089bb49e597 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -167,6 +167,7 @@ class GraphicsContextBase: def get_hatch_color(self) -> ColorType: ... def set_hatch_color(self, hatch_color: ColorType) -> None: ... def get_hatch_linewidth(self) -> float: ... + def set_hatch_linewidth(self, hatch_linewidth: float) -> None: ... def get_sketch_params(self) -> tuple[float, float, float] | None: ... def set_sketch_params( self, @@ -199,13 +200,11 @@ class TimerBase: class Event: name: str canvas: FigureCanvasBase + guiEvent: Any def __init__( self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ... ) -> None: ... - @property - def guiEvent(self) -> Any: ... - class DrawEvent(Event): renderer: RendererBase def __init__( @@ -220,7 +219,6 @@ class ResizeEvent(Event): class CloseEvent(Event): ... class LocationEvent(Event): - lastevent: Event | None x: int y: int inaxes: Axes | None @@ -261,6 +259,7 @@ class MouseEvent(LocationEvent): dblclick: bool = ..., guiEvent: Any | None = ..., *, + buttons: Iterable[MouseButton] | None = ..., modifiers: Iterable[str] | None = ..., ) -> None: ... @@ -349,7 +348,6 @@ class FigureCanvasBase: def get_default_filetype(cls) -> str: ... def get_default_filename(self) -> str: ... _T = TypeVar("_T", bound=FigureCanvasBase) - def switch_backends(self, FigureCanvasClass: type[_T]) -> _T: ... def mpl_connect(self, s: str, func: Callable[[Event], Any]) -> int: ... def mpl_disconnect(self, cid: int) -> None: ... def new_timer( @@ -406,6 +404,7 @@ class _Mode(str, Enum): class NavigationToolbar2: toolitems: tuple[tuple[str, ...] | tuple[None, ...], ...] + UNKNOWN_SAVED_STATUS: object canvas: FigureCanvasBase mode: _Mode def __init__(self, canvas: FigureCanvasBase) -> None: ... @@ -441,7 +440,7 @@ class NavigationToolbar2: def push_current(self) -> None: ... subplot_tool: widgets.SubplotTool def configure_subplots(self, *args): ... - def save_figure(self, *args) -> None: ... + def save_figure(self, *args) -> str | None | object: ... def update(self) -> None: ... def set_history_buttons(self) -> None: ... diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 5076ae563509..87ed794022a0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -261,9 +261,9 @@ def __init__(self, *args, **kwargs): self._last_cursor = self._default_cursor self.toolmanager.toolmanager_connect('tool_added_event', self._add_tool_cbk) - # process current tools - for tool in self.toolmanager.tools.values(): - self._add_tool(tool) + for tool in self.toolmanager.tools.values(): # process current tools + self._add_tool_cbk(mpl.backend_managers.ToolEvent( + 'tool_added_event', self.toolmanager, tool)) def set_figure(self, figure): if self._id_drag: @@ -273,24 +273,15 @@ def set_figure(self, figure): self._id_drag = self.canvas.mpl_connect( 'motion_notify_event', self._set_cursor_cbk) - def _tool_trigger_cbk(self, event): - if event.tool.toggled: - self._current_tool = event.tool - else: - self._current_tool = None - self._set_cursor_cbk(event.canvasevent) - - def _add_tool(self, tool): - """Set the cursor when the tool is triggered.""" - if getattr(tool, 'cursor', None) is not None: - self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, - self._tool_trigger_cbk) - def _add_tool_cbk(self, event): """Process every newly added tool.""" - if event.tool is self: - return - self._add_tool(event.tool) + if getattr(event.tool, 'cursor', None) is not None: + self.toolmanager.toolmanager_connect( + f'tool_trigger_{event.tool.name}', self._tool_trigger_cbk) + + def _tool_trigger_cbk(self, event): + self._current_tool = event.tool if event.tool.toggled else None + self._set_cursor_cbk(event.canvasevent) def _set_cursor_cbk(self, event): if not event or not self.canvas: @@ -391,8 +382,8 @@ def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) # Trigger grid switching by temporarily setting :rc:`keymap.grid` # to a unique key and sending an appropriate event. - with cbook._setattr_cm(event, key=sentinel), \ - mpl.rc_context({'keymap.grid': sentinel}): + with (cbook._setattr_cm(event, key=sentinel), + mpl.rc_context({'keymap.grid': sentinel})): mpl.backend_bases.key_press_handler(event, self.figure.canvas) @@ -406,8 +397,8 @@ def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) # Trigger grid switching by temporarily setting :rc:`keymap.grid_minor` # to a unique key and sending an appropriate event. - with cbook._setattr_cm(event, key=sentinel), \ - mpl.rc_context({'keymap.grid_minor': sentinel}): + with (cbook._setattr_cm(event, key=sentinel), + mpl.rc_context({'keymap.grid_minor': sentinel})): mpl.backend_bases.key_press_handler(event, self.figure.canvas) diff --git a/lib/matplotlib/backend_tools.pyi b/lib/matplotlib/backend_tools.pyi index 446f713292e1..f86a207c7545 100644 --- a/lib/matplotlib/backend_tools.pyi +++ b/lib/matplotlib/backend_tools.pyi @@ -75,8 +75,8 @@ class ToolXScale(AxisScaleBase): def set_scale(self, ax, scale: str | ScaleBase) -> None: ... class ToolViewsPositions(ToolBase): - views: dict[Figure | Axes, cbook.Stack] - positions: dict[Figure | Axes, cbook.Stack] + views: dict[Figure | Axes, cbook._Stack] + positions: dict[Figure | Axes, cbook._Stack] home_views: dict[Figure, dict[Axes, tuple[float, float, float, float]]] def add_figure(self, figure: Figure) -> None: ... def clear(self, figure: Figure) -> None: ... diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index ce55df523d9d..a2a878d54156 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -4,6 +4,7 @@ from io import BytesIO import functools +import logging from fontTools import subset @@ -24,7 +25,6 @@ def get_glyphs_subset(fontfile, characters): Subset a TTF font Reads the named fontfile and restricts the font to the characters. - Returns a serialization of the subset font as file-like object. Parameters ---------- @@ -32,6 +32,12 @@ def get_glyphs_subset(fontfile, characters): Path to the font file characters : str Continuous set of characters to include in subset + + Returns + ------- + fontTools.ttLib.ttFont.TTFont + An open font object representing the subset, which needs to + be closed by the caller. """ options = subset.Options(glyph_names=True, recommended_glyphs=True) @@ -42,19 +48,51 @@ def get_glyphs_subset(fontfile, characters): 'PfEd', # FontForge personal table. 'BDF', # X11 BDF header. 'meta', # Metadata stores design/supported languages (meaningless for subsets). + 'MERG', # Merge Table. + 'TSIV', # Microsoft Visual TrueType extension. + 'Zapf', # Information about the individual glyphs in the font. + 'bdat', # The bitmap data table. + 'bloc', # The bitmap location table. + 'cidg', # CID to Glyph ID table (Apple Advanced Typography). + 'fdsc', # The font descriptors table. + 'feat', # Feature name table (Apple Advanced Typography). + 'fmtx', # The Font Metrics Table. + 'fond', # Data-fork font information (Apple Advanced Typography). + 'just', # The justification table (Apple Advanced Typography). + 'kerx', # An extended kerning table (Apple Advanced Typography). + 'ltag', # Language Tag. + 'morx', # Extended Glyph Metamorphosis Table. + 'trak', # Tracking table. + 'xref', # The cross-reference table (some Apple font tooling information). ] - # if fontfile is a ttc, specify font number if fontfile.endswith(".ttc"): options.font_number = 0 - with subset.load_font(fontfile, options) as font: - subsetter = subset.Subsetter(options=options) - subsetter.populate(text=characters) - subsetter.subset(font) - fh = BytesIO() - font.save(fh, reorderTables=False) - return fh + font = subset.load_font(fontfile, options) + subsetter = subset.Subsetter(options=options) + subsetter.populate(text=characters) + subsetter.subset(font) + return font + + +def font_as_file(font): + """ + Convert a TTFont object into a file-like object. + + Parameters + ---------- + font : fontTools.ttLib.ttFont.TTFont + A font object + + Returns + ------- + BytesIO + A file object with the font saved into it + """ + fh = BytesIO() + font.save(fh, reorderTables=False) + return fh class CharacterTracker: @@ -123,7 +161,7 @@ def get_text_width_height_descent(self, s, prop, ismath): return w, h, d else: font = self._get_font_ttf(prop) - font.set_text(s, 0.0, flags=ft2font.LOAD_NO_HINTING) + font.set_text(s, 0.0, flags=ft2font.LoadFlags.NO_HINTING) w, h = font.get_width_height() d = font.get_descent() scale = 1 / 64 @@ -139,7 +177,13 @@ def _get_font_afm(self, prop): def _get_font_ttf(self, prop): fnames = font_manager.fontManager._find_fonts_by_props(prop) - font = font_manager.get_font(fnames) - font.clear() - font.set_size(prop.get_size_in_points(), 72) - return font + try: + font = font_manager.get_font(fnames) + font.clear() + font.set_size(prop.get_size_in_points(), 72) + return font + except RuntimeError: + logging.getLogger(__name__).warning( + "The PostScript/PDF backend does not currently " + "support the selected font (%s).", fnames) + raise diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index df06440a9826..0bbff1379ffa 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -19,7 +19,7 @@ from matplotlib import _api, backend_tools, cbook, _c_internal_utils from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase, ToolContainerBase, cursors, _Mode, + TimerBase, ToolContainerBase, cursors, _Mode, MouseButton, CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf from . import _tkagg @@ -176,8 +176,7 @@ def __init__(self, figure=None, master=None): self._tkcanvas_image_region = self._tkcanvas.create_image( w//2, h//2, image=self._tkphoto) self._tkcanvas.bind("", self.resize) - if sys.platform == 'win32': - self._tkcanvas.bind("", self._update_device_pixel_ratio) + self._tkcanvas.bind("", self._update_device_pixel_ratio) self._tkcanvas.bind("", self.key_press) self._tkcanvas.bind("", self.motion_notify_event) self._tkcanvas.bind("", self.enter_notify_event) @@ -234,11 +233,15 @@ def filter_destroy(event): self._rubberband_rect_white = None def _update_device_pixel_ratio(self, event=None): - # Tk gives scaling with respect to 72 DPI, but Windows screens are - # scaled vs 96 dpi, and pixel ratio settings are given in whole - # percentages, so round to 2 digits. - ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) - if self._set_device_pixel_ratio(ratio): + ratio = None + if sys.platform == 'win32': + # Tk gives scaling with respect to 72 DPI, but Windows screens are + # scaled vs 96 dpi, and pixel ratio settings are given in whole + # percentages, so round to 2 digits. + ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) + elif sys.platform == "linux": + ratio = self._tkcanvas.winfo_fpixels('1i') / 96 + if ratio is not None and self._set_device_pixel_ratio(ratio): # The easiest way to resize the canvas is to resize the canvas # widget itself, since we implement all the logic for resizing the # canvas backing store on that event. @@ -293,6 +296,7 @@ def _event_mpl_coords(self, event): def motion_notify_event(self, event): MouseEvent("motion_notify_event", self, *self._event_mpl_coords(event), + buttons=self._mpl_buttons(event), modifiers=self._mpl_modifiers(event), guiEvent=event)._process() @@ -354,13 +358,33 @@ def scroll_event_windows(self, event): x, y, step=step, modifiers=self._mpl_modifiers(event), guiEvent=event)._process() + @staticmethod + def _mpl_buttons(event): # See _mpl_modifiers. + # NOTE: This fails to report multiclicks on macOS; only one button is + # reported (multiclicks work correctly on Linux & Windows). + modifiers = [ + # macOS appears to swap right and middle (look for "Swap buttons + # 2/3" in tk/macosx/tkMacOSXMouseEvent.c). + (MouseButton.LEFT, 1 << 8), + (MouseButton.RIGHT, 1 << 9), + (MouseButton.MIDDLE, 1 << 10), + (MouseButton.BACK, 1 << 11), + (MouseButton.FORWARD, 1 << 12), + ] if sys.platform == "darwin" else [ + (MouseButton.LEFT, 1 << 8), + (MouseButton.MIDDLE, 1 << 9), + (MouseButton.RIGHT, 1 << 10), + (MouseButton.BACK, 1 << 11), + (MouseButton.FORWARD, 1 << 12), + ] + # State *before* press/release. + return [name for name, mask in modifiers if event.state & mask] + @staticmethod def _mpl_modifiers(event, *, exclude=None): - # add modifier keys to the key string. Bit details originate from - # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm - # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; - # BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080; - # BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400; + # Add modifier keys to the key string. Bit values are inferred from + # the implementation of tkinter.Event.__repr__ (1, 2, 4, 8, ... = + # Shift, Lock, Control, Mod1, ..., Mod5, Button1, ..., Button5) # In general, the modifier key is excluded from the modifier flag, # however this is not the case on "darwin", so double check that # we aren't adding repeat modifier flags to a modifier key. @@ -867,7 +891,7 @@ def save_figure(self, *args): ) if fname in ["", ()]: - return + return None # Save dir for next time, unless empty str (i.e., use cwd). if initialdir != "": mpl.rcParams['savefig.directory'] = ( @@ -882,6 +906,7 @@ def save_figure(self, *args): try: self.canvas.figure.savefig(fname, format=extension) + return fname except Exception as e: tkinter.messagebox.showerror("Error saving file", str(e)) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 92253c02c1b5..c37427369267 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -31,8 +31,7 @@ from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.font_manager import fontManager as _fontManager, get_font -from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, - LOAD_DEFAULT, LOAD_NO_AUTOHINT) +from matplotlib.ft2font import LoadFlags from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase @@ -41,16 +40,16 @@ def get_hinting_flag(): mapping = { - 'default': LOAD_DEFAULT, - 'no_autohint': LOAD_NO_AUTOHINT, - 'force_autohint': LOAD_FORCE_AUTOHINT, - 'no_hinting': LOAD_NO_HINTING, - True: LOAD_FORCE_AUTOHINT, - False: LOAD_NO_HINTING, - 'either': LOAD_DEFAULT, - 'native': LOAD_NO_AUTOHINT, - 'auto': LOAD_FORCE_AUTOHINT, - 'none': LOAD_NO_HINTING, + 'default': LoadFlags.DEFAULT, + 'no_autohint': LoadFlags.NO_AUTOHINT, + 'force_autohint': LoadFlags.FORCE_AUTOHINT, + 'no_hinting': LoadFlags.NO_HINTING, + True: LoadFlags.FORCE_AUTOHINT, + False: LoadFlags.NO_HINTING, + 'either': LoadFlags.DEFAULT, + 'native': LoadFlags.NO_AUTOHINT, + 'auto': LoadFlags.FORCE_AUTOHINT, + 'none': LoadFlags.NO_HINTING, } return mapping[mpl.rcParams['text.hinting']] @@ -266,10 +265,6 @@ def buffer_rgba(self): def tostring_argb(self): return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes() - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes() - def clear(self): self._renderer.clear() @@ -398,16 +393,6 @@ def get_renderer(self): self._lastKey = key return self.renderer - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - """ - Get the image as RGB `bytes`. - - `draw` must be called at least once before this function will work and - to update the renderer for any subsequent changes to the Figure. - """ - return self.renderer.tostring_rgb() - def tostring_argb(self): """ Get the image as ARGB `bytes`. diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 49d34f5794e4..888f5a770f5d 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -6,8 +6,8 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook from matplotlib.backend_bases import ( - ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent, - ResizeEvent) + ToolContainerBase, MouseButton, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) try: import gi @@ -156,6 +156,7 @@ def key_release_event(self, widget, event): def motion_notify_event(self, widget, event): MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + buttons=self._mpl_buttons(event.state), modifiers=self._mpl_modifiers(event.state), guiEvent=event)._process() return False # finish event propagation? @@ -182,6 +183,18 @@ def size_allocate(self, widget, allocation): ResizeEvent("resize_event", self)._process() self.draw_idle() + @staticmethod + def _mpl_buttons(event_state): + modifiers = [ + (MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK), + (MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK), + (MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK), + (MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK), + (MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK), + ] + # State *before* press/release. + return [name for name, mask in modifiers if event_state & mask] + @staticmethod def _mpl_modifiers(event_state, *, exclude=None): modifiers = [ @@ -339,7 +352,7 @@ def __init__(self, canvas): def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", - parent=self.canvas.get_toplevel(), + transient_for=self.canvas.get_toplevel(), action=Gtk.FileChooserAction.SAVE, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK), @@ -371,16 +384,17 @@ def on_notify_filter(*args): fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0] dialog.destroy() if response != Gtk.ResponseType.OK: - return + return None # Save dir for next time, unless empty str (which means use cwd). if mpl.rcParams['savefig.directory']: mpl.rcParams['savefig.directory'] = os.path.dirname(fname) try: self.canvas.figure.savefig(fname, format=fmt) + return fname except Exception as e: dialog = Gtk.MessageDialog( - parent=self.canvas.get_toplevel(), message_format=str(e), - type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK) + transient_for=self.canvas.get_toplevel(), text=str(e), + message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK) dialog.run() dialog.destroy() diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index 90b38ffa4ec3..bb469b85783d 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -70,5 +70,5 @@ def blit(self, bbox=None): @_BackendGTK3.export -class _BackendGTK3Cairo(_BackendGTK3): +class _BackendGTK3Agg(_BackendGTK3): FigureCanvas = FigureCanvasGTK3Agg diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index dd86ab628ce7..6af301c2becb 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -5,8 +5,8 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook from matplotlib.backend_bases import ( - ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent, - CloseEvent) + ToolContainerBase, MouseButton, + KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent) try: import gi @@ -115,6 +115,7 @@ def scroll_event(self, controller, dx, dy): MouseEvent( "scroll_event", self, *self._mpl_coords(), step=dy, modifiers=self._mpl_modifiers(controller), + guiEvent=controller.get_current_event(), )._process() return True @@ -123,6 +124,7 @@ def button_press_event(self, controller, n_press, x, y): "button_press_event", self, *self._mpl_coords((x, y)), controller.get_current_button(), modifiers=self._mpl_modifiers(controller), + guiEvent=controller.get_current_event(), )._process() self.grab_focus() @@ -131,12 +133,14 @@ def button_release_event(self, controller, n_press, x, y): "button_release_event", self, *self._mpl_coords((x, y)), controller.get_current_button(), modifiers=self._mpl_modifiers(controller), + guiEvent=controller.get_current_event(), )._process() def key_press_event(self, controller, keyval, keycode, state): KeyEvent( "key_press_event", self, self._get_key(keyval, keycode, state), *self._mpl_coords(), + guiEvent=controller.get_current_event(), )._process() return True @@ -144,25 +148,30 @@ def key_release_event(self, controller, keyval, keycode, state): KeyEvent( "key_release_event", self, self._get_key(keyval, keycode, state), *self._mpl_coords(), + guiEvent=controller.get_current_event(), )._process() return True def motion_notify_event(self, controller, x, y): MouseEvent( "motion_notify_event", self, *self._mpl_coords((x, y)), + buttons=self._mpl_buttons(controller), modifiers=self._mpl_modifiers(controller), + guiEvent=controller.get_current_event(), )._process() def enter_notify_event(self, controller, x, y): LocationEvent( "figure_enter_event", self, *self._mpl_coords((x, y)), modifiers=self._mpl_modifiers(), + guiEvent=controller.get_current_event(), )._process() def leave_notify_event(self, controller): LocationEvent( "figure_leave_event", self, *self._mpl_coords(), modifiers=self._mpl_modifiers(), + guiEvent=controller.get_current_event(), )._process() def resize_event(self, area, width, height): @@ -174,6 +183,26 @@ def resize_event(self, area, width, height): ResizeEvent("resize_event", self)._process() self.draw_idle() + def _mpl_buttons(self, controller): + # NOTE: This spews "Broken accounting of active state" warnings on + # right click on macOS. + surface = self.get_native().get_surface() + is_over, x, y, event_state = surface.get_device_position( + self.get_display().get_default_seat().get_pointer()) + # NOTE: alternatively we could use + # event_state = controller.get_current_event_state() + # but for button_press/button_release this would report the state + # *prior* to the event rather than after it; the above reports the + # state *after* it. + mod_table = [ + (MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK), + (MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK), + (MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK), + (MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK), + (MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK), + ] + return {name for name, mask in mod_table if event_state & mask} + def _mpl_modifiers(self, controller=None): if controller is None: surface = self.get_native().get_surface() @@ -385,6 +414,7 @@ def on_response(dialog, response): msg.show() dialog.show() + return self.UNKNOWN_SAVED_STATUS class ToolbarGTK4(ToolContainerBase, Gtk.Box): diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index adb5b5691b23..6ea437a90ca1 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -142,6 +142,7 @@ def save_figure(self, *args): if mpl.rcParams['savefig.directory']: mpl.rcParams['savefig.directory'] = os.path.dirname(filename) self.canvas.figure.savefig(filename) + return filename class FigureManagerMac(_macosx.FigureManager, FigureManagerBase): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e3e09f034f5..c1c5eb8819be 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -35,8 +35,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, - LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font) +from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -617,7 +616,7 @@ def _get_pdf_charprocs(font_path, glyph_ids): conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's). procs = {} for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LOAD_NO_SCALE) + g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) # NOTE: We should be using round(), but instead use # "(x+.5).astype(int)" to keep backcompat with the old ttconv code # (this is different for negative x's). @@ -732,7 +731,7 @@ def __init__(self, filename, metadata=None): self._soft_mask_states = {} self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) self._soft_mask_groups = [] - self.hatchPatterns = {} + self._hatch_patterns = {} self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) self.gouraudTriangles = [] @@ -1185,7 +1184,7 @@ def embedTTFType3(font, characters, descriptor): def get_char_width(charcode): s = ord(cp1252.decoding_table[charcode]) width = font.load_char( - s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance + s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning @@ -1270,7 +1269,8 @@ def embedTTFType42(font, characters, descriptor): subset_str = "".join(chr(c) for c in characters) _log.debug("SUBSET %s characters: %s", filename, subset_str) - fontdata = _backend_pdf_ps.get_glyphs_subset(filename, subset_str) + with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: + fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( "SUBSET %s %d -> %d", filename, os.stat(filename).st_size, fontdata.getbuffer().nbytes @@ -1321,7 +1321,7 @@ def embedTTFType42(font, characters, descriptor): ccode = c gind = font.get_char_index(ccode) glyph = font.load_char(ccode, - flags=LOAD_NO_SCALE | LOAD_NO_HINTING) + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1417,7 +1417,7 @@ def embedTTFType42(font, characters, descriptor): flags = 0 symbolic = False # ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10') - if ff & FIXED_WIDTH: + if FaceFlags.FIXED_WIDTH in ff: flags |= 1 << 0 if 0: # TODO: serif flags |= 1 << 1 @@ -1425,7 +1425,7 @@ def embedTTFType42(font, characters, descriptor): flags |= 1 << 2 else: flags |= 1 << 5 - if sf & ITALIC: + if StyleFlags.ITALIC in sf: flags |= 1 << 6 if 0: # TODO: all caps flags |= 1 << 16 @@ -1534,26 +1534,29 @@ def _write_soft_mask_groups(self): def hatchPattern(self, hatch_style): # The colors may come in as numpy arrays, which aren't hashable - if hatch_style is not None: - edge, face, hatch = hatch_style - if edge is not None: - edge = tuple(edge) - if face is not None: - face = tuple(face) - hatch_style = (edge, face, hatch) - - pattern = self.hatchPatterns.get(hatch_style, None) + edge, face, hatch, lw = hatch_style + if edge is not None: + edge = tuple(edge) + if face is not None: + face = tuple(face) + hatch_style = (edge, face, hatch, lw) + + pattern = self._hatch_patterns.get(hatch_style, None) if pattern is not None: return pattern name = next(self._hatch_pattern_seq) - self.hatchPatterns[hatch_style] = name + self._hatch_patterns[hatch_style] = name return name + hatchPatterns = _api.deprecated("3.10")(property(lambda self: { + k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() + })) + def writeHatches(self): hatchDict = dict() sidelen = 72.0 - for hatch_style, name in self.hatchPatterns.items(): + for hatch_style, name in self._hatch_patterns.items(): ob = self.reserveObject('hatch pattern') hatchDict[name] = ob res = {'Procsets': @@ -1568,7 +1571,7 @@ def writeHatches(self): # Change origin to match Agg at top-left. 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) - stroke_rgb, fill_rgb, hatch = hatch_style + stroke_rgb, fill_rgb, hatch, lw = hatch_style self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_stroke) if fill_rgb is not None: @@ -1577,7 +1580,7 @@ def writeHatches(self): 0, 0, sidelen, sidelen, Op.rectangle, Op.fill) - self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth) + self.output(lw, Op.setlinewidth) self.output(*self.pathOperations( Path.hatch(hatch), @@ -2378,7 +2381,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): multibyte_glyphs = [] prev_was_multibyte = True prev_font = font - for item in _text_helpers.layout(s, font, kern_mode=KERNING_UNFITTED): + for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): if _font_supports_glyph(fonttype, ord(item.char)): if prev_was_multibyte or item.ft_object != prev_font: singlebyte_chunks.append((item.ft_object, item.x, [])) @@ -2508,14 +2511,14 @@ def alpha_cmd(self, alpha, forced, effective_alphas): name = self.file.alphaState(effective_alphas) return [name, Op.setgstate] - def hatch_cmd(self, hatch, hatch_color): + def hatch_cmd(self, hatch, hatch_color, hatch_linewidth): if not hatch: if self._fillcolor is not None: return self.fillcolor_cmd(self._fillcolor) else: return [Name('DeviceRGB'), Op.setcolorspace_nonstroke] else: - hatch_style = (hatch_color, self._fillcolor, hatch) + hatch_style = (hatch_color, self._fillcolor, hatch, hatch_linewidth) name = self.file.hatchPattern(hatch_style) return [Name('Pattern'), Op.setcolorspace_nonstroke, name, Op.setcolor_nonstroke] @@ -2580,8 +2583,8 @@ def clip_cmd(self, cliprect, clippath): (('_dashes',), dash_cmd), (('_rgb',), rgb_cmd), # must come after fillcolor and rgb - (('_hatch', '_hatch_color'), hatch_cmd), - ) + (('_hatch', '_hatch_color', '_hatch_linewidth'), hatch_cmd), + ) def delta(self, other): """ @@ -2609,11 +2612,11 @@ def delta(self, other): break # Need to update hatching if we also updated fillcolor - if params == ('_hatch', '_hatch_color') and fill_performed: + if cmd.__name__ == 'hatch_cmd' and fill_performed: different = True if different: - if params == ('_fillcolor',): + if cmd.__name__ == 'fillcolor_cmd': fill_performed = True theirs = [getattr(other, p) for p in params] cmds.extend(cmd(self, *theirs)) @@ -2663,9 +2666,9 @@ class PdfPages: confusion when using `~.pyplot.savefig` and forgetting the format argument. """ - _UNSET = object() - - def __init__(self, filename, keep_empty=_UNSET, metadata=None): + @_api.delete_parameter("3.10", "keep_empty", + addendum="This parameter does nothing.") + def __init__(self, filename, keep_empty=None, metadata=None): """ Create a new PdfPages object. @@ -2676,10 +2679,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): The file is opened when a figure is saved for the first time (overwriting any older file with the same name). - keep_empty : bool, optional - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -2693,13 +2692,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): self._filename = filename self._metadata = metadata self._file = None - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty - - keep_empty = _api.deprecate_privatize_attribute("3.8") def __enter__(self): return self @@ -2721,11 +2713,6 @@ def close(self): self._file.finalize() self._file.close() self._file = None - elif self._keep_empty: # True *or* UNSET. - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - PdfFile(self._filename, metadata=self._metadata).close() # touch the file. def infodict(self): """ diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 9705f5fc6bce..48b6e8ac152c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -14,7 +14,7 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, cbook, font_manager as fm +from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase ) @@ -23,28 +23,47 @@ _create_pdf_info_dict, _datetime_to_pdf) from matplotlib.path import Path from matplotlib.figure import Figure +from matplotlib.font_manager import FontProperties from matplotlib._pylab_helpers import Gcf _log = logging.getLogger(__name__) +_DOCUMENTCLASS = r"\documentclass{article}" + + # Note: When formatting floating point values, it is important to use the # %f/{:f} format rather than %s/{} to avoid triggering scientific notation, # which is not recognized by TeX. def _get_preamble(): """Prepare a LaTeX preamble based on the rcParams configuration.""" + font_size_pt = FontProperties( + size=mpl.rcParams["font.size"] + ).get_size_in_points() return "\n".join([ # Remove Matplotlib's custom command \mathdefault. (Not using # \mathnormal instead since this looks odd with Computer Modern.) r"\def\mathdefault#1{#1}", # Use displaystyle for all math. r"\everymath=\expandafter{\the\everymath\displaystyle}", + # Set up font sizes to match font.size setting. + # If present, use the KOMA package scrextend to adjust the standard + # LaTeX font commands (\tiny, ..., \normalsize, ..., \Huge) accordingly. + # Otherwise, only set \normalsize, manually. + r"\IfFileExists{scrextend.sty}{", + r" \usepackage[fontsize=%fpt]{scrextend}" % font_size_pt, + r"}{", + r" \renewcommand{\normalsize}{\fontsize{%f}{%f}\selectfont}" + % (font_size_pt, 1.2 * font_size_pt), + r" \normalsize", + r"}", # Allow pgf.preamble to override the above definitions. mpl.rcParams["pgf.preamble"], - r"\ifdefined\pdftexversion\else % non-pdftex case.", - r" \usepackage{fontspec}", *([ + r"\ifdefined\pdftexversion\else % non-pdftex case.", + r" \usepackage{fontspec}", + ] + [ r" \%s{%s}[Path=\detokenize{%s/}]" % (command, path.name, path.parent.as_posix()) for command, path in zip( @@ -52,8 +71,7 @@ def _get_preamble(): [pathlib.Path(fm.findfont(family)) for family in ["serif", "sans\\-serif", "monospace"]] ) - ] if mpl.rcParams["pgf.rcfonts"] else []), - r"\fi", + ] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), # Documented as "must come last". mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"), ]) @@ -94,6 +112,8 @@ def _escape_and_apply_props(s, prop): family = prop.get_family()[0] if family in families: commands.append(families[family]) + elif not mpl.rcParams["pgf.rcfonts"]: + commands.append(r"\fontfamily{\familydefault}") elif any(font.name == family for font in fm.fontManager.ttflist): commands.append( r"\ifdefined\pdftexversion\else\setmainfont{%s}\rmfamily\fi" % family) @@ -185,7 +205,7 @@ class LatexManager: @staticmethod def _build_latex_header(): latex_header = [ - r"\documentclass{article}", + _DOCUMENTCLASS, # Include TeX program name as a comment for cache invalidation. # TeX does not allow this to be the first line. rf"% !TeX program = {mpl.rcParams['pgf.texsystem']}", @@ -814,7 +834,7 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): self.print_pgf(tmppath / "figure.pgf", **kwargs) (tmppath / "figure.tex").write_text( "\n".join([ - r"\documentclass[12pt]{article}", + _DOCUMENTCLASS, r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo, r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}" % (w, h), @@ -829,8 +849,8 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", "figure.tex"], _log, cwd=tmpdir) - with (tmppath / "figure.pdf").open("rb") as orig, \ - cbook.open_file_cm(fname_or_fh, "wb") as dest: + with ((tmppath / "figure.pdf").open("rb") as orig, + cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target def print_png(self, fname_or_fh, **kwargs): @@ -842,8 +862,8 @@ def print_png(self, fname_or_fh, **kwargs): png_path = tmppath / "figure.png" self.print_pdf(pdf_path, **kwargs) converter(pdf_path, png_path, dpi=self.figure.dpi) - with png_path.open("rb") as orig, \ - cbook.open_file_cm(fname_or_fh, "wb") as dest: + with (png_path.open("rb") as orig, + cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target def get_renderer(self): @@ -878,9 +898,7 @@ class PdfPages: ... pdf.savefig() """ - _UNSET = object() - - def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): + def __init__(self, filename, *, metadata=None): """ Create a new PdfPages object. @@ -890,10 +908,6 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): Plots using `PdfPages.savefig` will be written to a file at this location. Any older file with the same name is overwritten. - keep_empty : bool, default: True - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -909,22 +923,15 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): """ self._output_name = filename self._n_figures = 0 - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty self._metadata = (metadata or {}).copy() self._info_dict = _create_pdf_info_dict('pgf', self._metadata) self._file = BytesIO() - keep_empty = _api.deprecate_privatize_attribute("3.8") - def _write_header(self, width_inches, height_inches): pdfinfo = ','.join( _metadata_to_str(k, v) for k, v in self._info_dict.items()) latex_header = "\n".join([ - r"\documentclass[12pt]{article}", + _DOCUMENTCLASS, r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo, r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}" % (width_inches, height_inches), @@ -949,11 +956,6 @@ def close(self): self._file.write(rb'\end{document}\n') if self._n_figures > 0: self._run_latex() - elif self._keep_empty: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - open(self._output_name, 'wb').close() self._file.close() def _run_latex(self): @@ -995,14 +997,10 @@ def savefig(self, figure=None, **kwargs): # luatex<0.85; they were renamed to \pagewidth and \pageheight # on luatex>=0.85. self._file.write( - ( - r'\newpage' - r'\ifdefined\pdfpagewidth\pdfpagewidth' - fr'\else\pagewidth\fi={width}in' - r'\ifdefined\pdfpageheight\pdfpageheight' - fr'\else\pageheight\fi={height}in' - '%%\n' - ).encode("ascii") + rb'\newpage' + rb'\ifdefined\pdfpagewidth\pdfpagewidth\else\pagewidth\fi=%fin' + rb'\ifdefined\pdfpageheight\pdfpageheight\else\pageheight\fi=%fin' + b'%%\n' % (width, height) ) figure.savefig(self._file, format="pgf", backend="pgf", **kwargs) self._n_figures += 1 diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 5f224f38af1e..f1f914ae5420 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -2,6 +2,7 @@ A PostScript backend, which can produce both PostScript .ps and .eps. """ +import bisect import codecs import datetime from enum import Enum @@ -13,9 +14,12 @@ import os import pathlib import shutil +import struct from tempfile import TemporaryDirectory +import textwrap import time +import fontTools import numpy as np import matplotlib as mpl @@ -25,8 +29,7 @@ _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.cbook import is_writable_file_like, file_requires_unicode from matplotlib.font_manager import get_font -from matplotlib.ft2font import LOAD_NO_SCALE, FT2Font -from matplotlib._ttconv import convert_ttf_to_ps +from matplotlib.ft2font import LoadFlags from matplotlib._mathtext_data import uni2type1 from matplotlib.path import Path from matplotlib.texmanager import TexManager @@ -39,12 +42,6 @@ debugPS = False -@_api.caching_module_getattr -class __getattr__: - # module-level deprecations - psDefs = _api.deprecated("3.8", obj_type="")(property(lambda self: _psDefs)) - - papersize = {'letter': (8.5, 11), 'legal': (8.5, 14), 'ledger': (11, 17), @@ -72,15 +69,6 @@ class __getattr__: 'b10': (1.26, 1.76)} -def _get_papertype(w, h): - for key, (pw, ph) in sorted(papersize.items(), reverse=True): - if key.startswith('l'): - continue - if w < pw and h < ph: - return key - return 'a0' - - def _nums_to_str(*args, sep=" "): return sep.join(f"{arg:1.3f}".rstrip("0").rstrip(".") for arg in args) @@ -160,7 +148,7 @@ def _font_to_ps_type3(font_path, chars): entries = [] for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LOAD_NO_SCALE) + g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { @@ -198,28 +186,201 @@ def _font_to_ps_type42(font_path, chars, fh): subset_str = ''.join(chr(c) for c in chars) _log.debug("SUBSET %s characters: %s", font_path, subset_str) try: - fontdata = _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) - _log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, - fontdata.getbuffer().nbytes) - - # Give ttconv a subsetted font along with updated glyph_ids. - font = FT2Font(fontdata) - glyph_ids = [font.get_char_index(c) for c in chars] - with TemporaryDirectory() as tmpdir: - tmpfile = os.path.join(tmpdir, "tmp.ttf") - - with open(tmpfile, 'wb') as tmp: - tmp.write(fontdata.getvalue()) - - # TODO: allow convert_ttf_to_ps to input file objects (BytesIO) - convert_ttf_to_ps(os.fsencode(tmpfile), fh, 42, glyph_ids) + kw = {} + # fix this once we support loading more fonts from a collection + # https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541 + if font_path.endswith('.ttc'): + kw['fontNumber'] = 0 + with (fontTools.ttLib.TTFont(font_path, **kw) as font, + _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset): + fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() + _log.debug( + "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, + len(fontdata) + ) + fh.write(_serialize_type42(font, subset, fontdata)) except RuntimeError: _log.warning( - "The PostScript backend does not currently " - "support the selected font.") + "The PostScript backend does not currently support the selected font (%s).", + font_path) raise +def _serialize_type42(font, subset, fontdata): + """ + Output a PostScript Type-42 format representation of font + + Parameters + ---------- + font : fontTools.ttLib.ttFont.TTFont + The original font object + subset : fontTools.ttLib.ttFont.TTFont + The subset font object + fontdata : bytes + The raw font data in TTF format + + Returns + ------- + str + The Type-42 formatted font + """ + version, breakpoints = _version_and_breakpoints(font.get('loca'), fontdata) + post = font['post'] + name = font['name'] + chars = _generate_charstrings(subset) + sfnts = _generate_sfnts(fontdata, subset, breakpoints) + return textwrap.dedent(f""" + %%!PS-TrueTypeFont-{version[0]}.{version[1]}-{font['head'].fontRevision:.7f} + 10 dict begin + /FontType 42 def + /FontMatrix [1 0 0 1 0 0] def + /FontName /{name.getDebugName(6)} def + /FontInfo 7 dict dup begin + /FullName ({name.getDebugName(4)}) def + /FamilyName ({name.getDebugName(1)}) def + /Version ({name.getDebugName(5)}) def + /ItalicAngle {post.italicAngle} def + /isFixedPitch {'true' if post.isFixedPitch else 'false'} def + /UnderlinePosition {post.underlinePosition} def + /UnderlineThickness {post.underlineThickness} def + end readonly def + /Encoding StandardEncoding def + /FontBBox [{_nums_to_str(*_bounds(font))}] def + /PaintType 0 def + /CIDMap 0 def + {chars} + {sfnts} + FontName currentdict end definefont pop + """) + + +def _version_and_breakpoints(loca, fontdata): + """ + Read the version number of the font and determine sfnts breakpoints. + + When a TrueType font file is written as a Type 42 font, it has to be + broken into substrings of at most 65535 bytes. These substrings must + begin at font table boundaries or glyph boundaries in the glyf table. + This function determines all possible breakpoints and it is the caller's + responsibility to do the splitting. + + Helper function for _font_to_ps_type42. + + Parameters + ---------- + loca : fontTools.ttLib._l_o_c_a.table__l_o_c_a or None + The loca table of the font if available + fontdata : bytes + The raw data of the font + + Returns + ------- + version : tuple[int, int] + A 2-tuple of the major version number and minor version number. + breakpoints : list[int] + The breakpoints is a sorted list of offsets into fontdata; if loca is not + available, just the table boundaries. + """ + v1, v2, numTables = struct.unpack('>3h', fontdata[:6]) + version = (v1, v2) + + tables = {} + for i in range(numTables): + tag, _, offset, _ = struct.unpack('>4sIII', fontdata[12 + i*16:12 + (i+1)*16]) + tables[tag.decode('ascii')] = offset + if loca is not None: + glyf_breakpoints = {tables['glyf'] + offset for offset in loca.locations[:-1]} + else: + glyf_breakpoints = set() + breakpoints = sorted({*tables.values(), *glyf_breakpoints, len(fontdata)}) + + return version, breakpoints + + +def _bounds(font): + """ + Compute the font bounding box, as if all glyphs were written + at the same start position. + + Helper function for _font_to_ps_type42. + + Parameters + ---------- + font : fontTools.ttLib.ttFont.TTFont + The font + + Returns + ------- + tuple + (xMin, yMin, xMax, yMax) of the combined bounding box + of all the glyphs in the font + """ + gs = font.getGlyphSet(False) + pen = fontTools.pens.boundsPen.BoundsPen(gs) + for name in gs.keys(): + gs[name].draw(pen) + return pen.bounds or (0, 0, 0, 0) + + +def _generate_charstrings(font): + """ + Transform font glyphs into CharStrings + + Helper function for _font_to_ps_type42. + + Parameters + ---------- + font : fontTools.ttLib.ttFont.TTFont + The font + + Returns + ------- + str + A definition of the CharStrings dictionary in PostScript + """ + go = font.getGlyphOrder() + s = f'/CharStrings {len(go)} dict dup begin\n' + for i, name in enumerate(go): + s += f'/{name} {i} def\n' + s += 'end readonly def' + return s + + +def _generate_sfnts(fontdata, font, breakpoints): + """ + Transform font data into PostScript sfnts format. + + Helper function for _font_to_ps_type42. + + Parameters + ---------- + fontdata : bytes + The raw data of the font + font : fontTools.ttLib.ttFont.TTFont + The fontTools font object + breakpoints : list + Sorted offsets of possible breakpoints + + Returns + ------- + str + The sfnts array for the font definition, consisting + of hex-encoded strings in PostScript format + """ + s = '/sfnts[' + pos = 0 + while pos < len(fontdata): + i = bisect.bisect_left(breakpoints, pos + 65534) + newpos = breakpoints[i-1] + if newpos <= pos: + # have to accept a larger string + newpos = breakpoints[-1] + s += f'<{fontdata[pos:newpos].hex()}00>' # Always NUL terminate. + pos = newpos + s += ']def' + return '\n'.join(s[i:i+100] for i in range(0, len(s), 100)) + + def _log_if_debug_on(meth): """ Wrap `RendererPS` method *meth* to emit a PS comment with the method name, @@ -344,12 +505,11 @@ def set_font(self, fontname, fontsize, store=True): self.fontname = fontname self.fontsize = fontsize - def create_hatch(self, hatch): + def create_hatch(self, hatch, linewidth): sidelen = 72 if hatch in self._hatches: return self._hatches[hatch] name = 'H%d' % len(self._hatches) - linewidth = mpl.rcParams['hatch.linewidth'] pageheight = self.height * 72 self._pswriter.write(f"""\ << /PatternType 1 @@ -772,7 +932,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): write("grestore\n") if hatch: - hatch_name = self.create_hatch(hatch) + hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth()) write("gsave\n") write(_nums_to_str(*gc.get_hatch_color()[:3])) write(f" {hatch_name} setpattern fill grestore\n") @@ -828,7 +988,7 @@ def _print_ps( if papertype is None: papertype = mpl.rcParams['ps.papersize'] papertype = papertype.lower() - _api.check_in_list(['figure', 'auto', *papersize], papertype=papertype) + _api.check_in_list(['figure', *papersize], papertype=papertype) orientation = _api.check_getitem( _Orientation, orientation=orientation.lower()) @@ -858,9 +1018,6 @@ def _print_figure( # find the appropriate papertype width, height = self.figure.get_size_inches() - if papertype == 'auto': - papertype = _get_papertype(*orientation.swap_if_landscape((width, height))) - if is_eps or papertype == 'figure': paper_width, paper_height = width, height else: @@ -1041,8 +1198,6 @@ def _print_figure_tex( paper_width, paper_height = orientation.swap_if_landscape( self.figure.get_size_inches()) else: - if papertype == 'auto': - papertype = _get_papertype(width, height) paper_width, paper_height = papersize[papertype] psfrag_rotated = _convert_psfrags( diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 242c6fdbf9f9..432bbb1ffdb6 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -329,6 +329,7 @@ def mouseMoveEvent(self, event): return MouseEvent("motion_notify_event", self, *self.mouseEventCoords(event), + buttons=self._mpl_buttons(event.buttons()), modifiers=self._mpl_modifiers(), guiEvent=event)._process() @@ -396,6 +397,13 @@ def sizeHint(self): def minimumSizeHint(self): return QtCore.QSize(10, 10) + @staticmethod + def _mpl_buttons(buttons): + buttons = _to_int(buttons) + # State *after* press/release. + return {button for mask, button in FigureCanvasQT.buttond.items() + if _to_int(mask) & buttons} + @staticmethod def _mpl_modifiers(modifiers=None, *, exclude=None): if modifiers is None: @@ -483,7 +491,7 @@ def blit(self, bbox=None): if bbox is None and self.figure: bbox = self.figure.bbox # Blit the entire canvas if bbox is None. # repaint uses logical pixels, not physical pixels like the renderer. - l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds] + l, b, w, h = (int(pt / self.device_pixel_ratio) for pt in bbox.bounds) t = b + h self.repaint(l, self.rect().height() - t, w, h) @@ -504,7 +512,7 @@ def drawRectangle(self, rect): # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs # to be called at the end of paintEvent. if rect is not None: - x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect] + x0, y0, w, h = (int(pt / self.device_pixel_ratio) for pt in rect) x1 = x0 + w y1 = y0 + h def _draw_rect_callback(painter): @@ -658,9 +666,6 @@ def set_window_title(self, title): class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): - _message = QtCore.Signal(str) # Remove once deprecation below elapses. - message = _api.deprecate_privatize_attribute("3.8") - toolitems = [*NavigationToolbar2.toolitems] toolitems.insert( # Add 'customize' action after 'subplots' @@ -783,7 +788,6 @@ def zoom(self, *args): self._update_buttons_checked() def set_message(self, s): - self._message.emit(s) if self.coordinates: self.locLabel.setText(s) @@ -839,6 +843,7 @@ def save_figure(self, *args): self, "Error saving file", str(e), QtWidgets.QMessageBox.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.NoButton) + return fname def set_history_buttons(self): can_backward = self._nav_stack._pos > 0 @@ -851,7 +856,7 @@ def set_history_buttons(self): class SubplotToolQt(QtWidgets.QDialog): def __init__(self, targetfig, parent): - super().__init__() + super().__init__(parent) self.setWindowIcon(QtGui.QIcon( str(cbook._get_data_path("images/matplotlib.png")))) self.setObjectName("SubplotTool") diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 72354b81862b..2193dc6b6cdc 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -302,6 +302,7 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, self._groupd = {} self._image_counter = itertools.count() + self._clip_path_ids = {} self._clipd = {} self._markers = {} self._path_collection_id = 0 @@ -321,10 +322,25 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, viewBox=f'0 0 {str_width} {str_height}', xmlns="http://www.w3.org/2000/svg", version="1.1", + id=mpl.rcParams['svg.id'], attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_metadata(metadata) self._write_default_style() + def _get_clippath_id(self, clippath): + """ + Returns a stable and unique identifier for the *clippath* argument + object within the current rendering context. + + This allows plots that include custom clip paths to produce identical + SVG output on each render, provided that the :rc:`svg.hashsalt` config + setting and the ``SOURCE_DATE_EPOCH`` build-time environment variable + are set to fixed values. + """ + if clippath not in self._clip_path_ids: + self._clip_path_ids[clippath] = len(self._clip_path_ids) + return self._clip_path_ids[clippath] + def finalize(self): self._write_clips() self._write_hatches() @@ -484,11 +500,12 @@ def _get_hatch(self, gc, rgbFace): edge = gc.get_hatch_color() if edge is not None: edge = tuple(edge) - dictkey = (gc.get_hatch(), rgbFace, edge) + lw = gc.get_hatch_linewidth() + dictkey = (gc.get_hatch(), rgbFace, edge, lw) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) - self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) + self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge, lw), oid) else: _, oid = oid return oid @@ -499,7 +516,7 @@ def _write_hatches(self): HATCH_SIZE = 72 writer = self.writer writer.start('defs') - for (path, face, stroke), oid in self._hatchd.values(): + for (path, face, stroke, lw), oid in self._hatchd.values(): writer.start( 'pattern', id=oid, @@ -523,7 +540,7 @@ def _write_hatches(self): hatch_style = { 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), - 'stroke-width': str(mpl.rcParams['hatch.linewidth']), + 'stroke-width': str(lw), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' } @@ -590,7 +607,7 @@ def _get_clip_attrs(self, gc): clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) - dictkey = (id(clippath), str(clippath_trans)) + dictkey = (self._get_clippath_id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) @@ -605,7 +622,7 @@ def _get_clip_attrs(self, gc): else: self._clipd[dictkey] = (dictkey, oid) else: - clip, oid = clip + _, oid = clip return {'clip-path': f'url(#{oid})'} def _write_clips(self): @@ -699,6 +716,8 @@ def draw_markers( self._markers[dictkey] = oid writer.start('g', **self._get_clip_attrs(gc)) + if gc.get_url() is not None: + self.writer.start('a', {'xlink:href': gc.get_url()}) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': f'#{oid}'} clip = (0, 0, self.width*72, self.height*72) @@ -710,6 +729,8 @@ def draw_markers( attrib['y'] = _short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) + if gc.get_url() is not None: + self.writer.end('a') writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, @@ -1050,12 +1071,13 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: - attrib = {'xlink:href': f'#{glyph_id}'} - if xposition != 0.0: - attrib['x'] = _short_float_fmt(xposition) - if yposition != 0.0: - attrib['y'] = _short_float_fmt(yposition) - writer.element('use', attrib=attrib) + writer.element( + 'use', + transform=_generate_transform([ + ('translate', (xposition, yposition)), + ('scale', (scale,)), + ]), + attrib={'xlink:href': f'#{glyph_id}'}) else: if ismath == "TeX": @@ -1093,25 +1115,26 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) - style = {} + font_style = {} + color_style = {} if color != '#000000': - style['fill'] = color + color_style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - style['opacity'] = _short_float_fmt(alpha) + color_style['opacity'] = _short_float_fmt(alpha) if not ismath: attrib = {} - font_parts = [] + # Separate font style in their separate attributes if prop.get_style() != 'normal': - font_parts.append(prop.get_style()) + font_style['font-style'] = prop.get_style() if prop.get_variant() != 'normal': - font_parts.append(prop.get_variant()) + font_style['font-variant'] = prop.get_variant() weight = fm.weight_dict[prop.get_weight()] if weight != 400: - font_parts.append(f'{weight}') + font_style['font-weight'] = f'{weight}' def _normalize_sans(name): return 'sans-serif' if name in ['sans', 'sans serif'] else name @@ -1134,15 +1157,15 @@ def _get_all_quoted_names(prop): for entry in prop.get_family() for name in _expand_family_entry(entry)] - font_parts.extend([ - f'{_short_float_fmt(prop.get_size())}px', - # ensure expansion, quoting, and dedupe of font names - ", ".join(dict.fromkeys(_get_all_quoted_names(prop))) - ]) - style['font'] = ' '.join(font_parts) + font_style['font-size'] = f'{_short_float_fmt(prop.get_size())}px' + # ensure expansion, quoting, and dedupe of font names + font_style['font-family'] = ", ".join( + dict.fromkeys(_get_all_quoted_names(prop)) + ) + if prop.get_stretch() != 'normal': - style['font-stretch'] = prop.get_stretch() - attrib['style'] = _generate_css(style) + font_style['font-stretch'] = prop.get_stretch() + attrib['style'] = _generate_css({**font_style, **color_style}) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original @@ -1164,11 +1187,11 @@ def _get_all_quoted_names(prop): ha_mpl_to_svg = {'left': 'start', 'right': 'end', 'center': 'middle'} - style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] + font_style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] attrib['x'] = _short_float_fmt(ax) attrib['y'] = _short_float_fmt(ay) - attrib['style'] = _generate_css(style) + attrib['style'] = _generate_css({**font_style, **color_style}) attrib['transform'] = _generate_transform([ ("rotate", (-angle, ax, ay))]) @@ -1188,7 +1211,7 @@ def _get_all_quoted_names(prop): # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. writer.start('g', - style=_generate_css(style), + style=_generate_css({**font_style, **color_style}), transform=_generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]), @@ -1200,43 +1223,32 @@ def _get_all_quoted_names(prop): spans = {} for font, fontsize, thetext, new_x, new_y in glyphs: entry = fm.ttfFontProperty(font) - font_parts = [] + font_style = {} + # Separate font style in its separate attributes if entry.style != 'normal': - font_parts.append(entry.style) + font_style['font-style'] = entry.style if entry.variant != 'normal': - font_parts.append(entry.variant) + font_style['font-variant'] = entry.variant if entry.weight != 400: - font_parts.append(f'{entry.weight}') - font_parts.extend([ - f'{_short_float_fmt(fontsize)}px', - f'{entry.name!r}', # ensure quoting - ]) - style = {'font': ' '.join(font_parts)} + font_style['font-weight'] = f'{entry.weight}' + font_style['font-size'] = f'{_short_float_fmt(fontsize)}px' + font_style['font-family'] = f'{entry.name!r}' # ensure quoting if entry.stretch != 'normal': - style['font-stretch'] = entry.stretch - style = _generate_css(style) + font_style['font-stretch'] = entry.stretch + style = _generate_css({**font_style, **color_style}) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) for style, chars in spans.items(): - chars.sort() - - if len({y for x, y, t in chars}) == 1: # Are all y's the same? - ys = str(chars[0][1]) - else: - ys = ' '.join(str(c[1]) for c in chars) - - attrib = { - 'style': style, - 'x': ' '.join(_short_float_fmt(c[0]) for c in chars), - 'y': ys - } - - writer.element( - 'tspan', - ''.join(chr(c[2]) for c in chars), - attrib=attrib) + chars.sort() # Sort by increasing x position + for x, y, t in chars: # Output one tspan for each character + writer.element( + 'tspan', + chr(t), + x=_short_float_fmt(x), + y=_short_float_fmt(y), + style=style) writer.end('text') @@ -1340,8 +1352,8 @@ def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None): renderer.finalize() def print_svgz(self, filename, **kwargs): - with cbook.open_file_cm(filename, "wb") as fh, \ - gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter: + with (cbook.open_file_cm(filename, "wb") as fh, + gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter): return self.print_svg(gzipwriter, **kwargs) def get_default_filetype(self): diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index d997ec160a53..83aa6bb567c1 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -20,8 +20,8 @@ import matplotlib matplotlib.use("module://my.backend") -If your backend implements support for saving figures (i.e. has a `print_xyz` -method), you can register it as the default handler for a given file type:: +If your backend implements support for saving figures (i.e. has a ``print_xyz`` method), +you can register it as the default handler for a given file type:: from matplotlib.backend_bases import register_backend register_backend('xyz', 'my_backend', 'XYZ File Format') diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 4ceac1699543..12906827a466 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -22,7 +22,7 @@ from matplotlib import _api, backend_bases, backend_tools from matplotlib.backends import backend_agg from matplotlib.backend_bases import ( - _Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) + _Backend, MouseButton, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) _log = logging.getLogger(__name__) @@ -283,10 +283,17 @@ def _handle_mouse(self, event): y = event['y'] y = self.get_renderer().height - y self._last_mouse_xy = x, y - # JavaScript button numbers and Matplotlib button numbers are off by 1. - button = event['button'] + 1 - e_type = event['type'] + button = event['button'] + 1 # JS numbers off by 1 compared to mpl. + buttons = { # JS ordering different compared to mpl. + button for button, mask in [ + (MouseButton.LEFT, 1), + (MouseButton.RIGHT, 2), + (MouseButton.MIDDLE, 4), + (MouseButton.BACK, 8), + (MouseButton.FORWARD, 16), + ] if event['buttons'] & mask # State *after* press/release. + } modifiers = event['modifiers'] guiEvent = event.get('guiEvent') if e_type in ['button_press', 'button_release']: @@ -300,10 +307,12 @@ def _handle_mouse(self, event): modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'motion_notify': MouseEvent(e_type + '_event', self, x, y, - modifiers=modifiers, guiEvent=guiEvent)._process() + buttons=buttons, modifiers=modifiers, guiEvent=guiEvent, + )._process() elif e_type in ['figure_enter', 'figure_leave']: LocationEvent(e_type + '_event', self, x, y, modifiers=modifiers, guiEvent=guiEvent)._process() + handle_button_press = handle_button_release = handle_dblclick = \ handle_figure_enter = handle_figure_leave = handle_motion_notify = \ handle_scroll = _handle_mouse @@ -403,8 +412,9 @@ def remove_rubberband(self): self.canvas.send_event("rubberband", x0=-1, y0=-1, x1=-1, y1=-1) def save_figure(self, *args): - """Save the current figure""" + """Save the current figure.""" self.canvas.send_event('save') + return self.UNKNOWN_SAVED_STATUS def pan(self): super().pan() diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index d39edf40f151..f83a69d8361e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -685,6 +685,22 @@ def _on_size(self, event): ResizeEvent("resize_event", self)._process() self.draw_idle() + @staticmethod + def _mpl_buttons(): + state = wx.GetMouseState() + # NOTE: Alternatively, we could use event.LeftIsDown() / etc. but this + # fails to report multiclick drags on macOS (other OSes have not been + # verified). + mod_table = [ + (MouseButton.LEFT, state.LeftIsDown()), + (MouseButton.RIGHT, state.RightIsDown()), + (MouseButton.MIDDLE, state.MiddleIsDown()), + (MouseButton.BACK, state.Aux1IsDown()), + (MouseButton.FORWARD, state.Aux2IsDown()), + ] + # State *after* press/release. + return {button for button, flag in mod_table if flag} + @staticmethod def _mpl_modifiers(event=None, *, exclude=None): mod_table = [ @@ -794,9 +810,8 @@ def _on_mouse_button(self, event): MouseEvent("button_press_event", self, x, y, button, modifiers=modifiers, guiEvent=event)._process() elif event.ButtonDClick(): - MouseEvent("button_press_event", self, x, y, button, - dblclick=True, modifiers=modifiers, - guiEvent=event)._process() + MouseEvent("button_press_event", self, x, y, button, dblclick=True, + modifiers=modifiers, guiEvent=event)._process() elif event.ButtonUp(): MouseEvent("button_release_event", self, x, y, button, modifiers=modifiers, guiEvent=event)._process() @@ -826,6 +841,7 @@ def _on_motion(self, event): event.Skip() MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + buttons=self._mpl_buttons(), modifiers=self._mpl_modifiers(event), guiEvent=event)._process() @@ -1143,6 +1159,7 @@ def save_figure(self, *args): mpl.rcParams["savefig.directory"] = str(path.parent) try: self.canvas.figure.savefig(path, format=fmt) + return path except Exception as e: dialog = wx.MessageDialog( parent=self.canvas.GetParent(), message=str(e), @@ -1197,8 +1214,8 @@ def _get_tool_pos(self, tool): ``ToolBar.GetToolPos`` is not useful because wx assigns the same Id to all Separators and StretchableSpaces. """ - pos, = [pos for pos in range(self.ToolsCount) - if self.GetToolByPos(pos) == tool] + pos, = (pos for pos in range(self.ToolsCount) + if self.GetToolByPos(pos) == tool) return pos def add_toolitem(self, name, group, position, image_file, description, diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index d91f7c14cb22..b57a98b1138a 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -49,7 +49,7 @@ if QT_API_ENV in ["pyqt5", "pyside2"]: QT_API = _ETS[QT_API_ENV] else: - _QT_FORCE_QT5_BINDING = True # noqa + _QT_FORCE_QT5_BINDING = True # noqa: F811 QT_API = None # A non-Qt backend was selected but we still got there (possible, e.g., when # fully manually embedding Matplotlib in a Qt app without using pyplot). diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 3c2eaf1bacd3..9d31fa9ced2c 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -42,7 +42,7 @@ def convert_limits(lim, converter): axis_map = axes._axis_map axis_limits = { name: tuple(convert_limits( - getattr(axes, f'get_{name}lim')(), axis.converter + getattr(axes, f'get_{name}lim')(), axis.get_converter() )) for name, axis in axis_map.items() } @@ -54,7 +54,7 @@ def convert_limits(lim, converter): (None, f"{name.title()}-Axis"), ('Min', axis_limits[name][0]), ('Max', axis_limits[name][1]), - ('Label', axis.get_label().get_text()), + ('Label', axis.label.get_text()), ('Scale', [axis.get_scale(), 'linear', 'log', 'symlog', 'logit']), sep, @@ -66,7 +66,7 @@ def convert_limits(lim, converter): # Save the converter and unit data axis_converter = { - name: axis.converter + name: axis.get_converter() for name, axis in axis_map.items() } axis_units = { @@ -165,7 +165,7 @@ def prepare_data(d, init): 'Interpolation', [mappable.get_interpolation(), *interpolations])) - interpolation_stages = ['data', 'rgba'] + interpolation_stages = ['data', 'rgba', 'auto'] mappabledata.append(( 'Interpolation stage', [mappable.get_interpolation_stage(), *interpolation_stages])) @@ -209,7 +209,7 @@ def apply_callback(data): axis.set_label_text(axis_label) # Restore the unit data - axis.converter = axis_converter[name] + axis._set_converter(axis_converter[name]) axis.set_units(axis_units[name]) # Set / Curves diff --git a/lib/matplotlib/backends/registry.py b/lib/matplotlib/backends/registry.py index e08817bb089b..3c85a9b47d7b 100644 --- a/lib/matplotlib/backends/registry.py +++ b/lib/matplotlib/backends/registry.py @@ -132,14 +132,8 @@ def _read_entry_points(self): # [project.entry-points."matplotlib.backend"] # inline = "matplotlib_inline.backend_inline" import importlib.metadata as im - import sys - - # entry_points group keyword not available before Python 3.10 - group = "matplotlib.backend" - if sys.version_info >= (3, 10): - entry_points = im.entry_points(group=group) - else: - entry_points = im.entry_points().get(group, ()) + + entry_points = im.entry_points(group="matplotlib.backend") entries = [(entry.name, entry.value) for entry in entry_points] # For backward compatibility, if matplotlib-inline and/or ipympl are installed diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 6e8ec449d92b..2d1f383e9839 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -644,6 +644,7 @@ mpl.figure.prototype.mouse_event = function (event, name) { y: y, button: event.button, step: event.step, + buttons: event.buttons, modifiers: getModifiers(event), guiEvent: simpleKeys(event), }); diff --git a/lib/matplotlib/bezier.py b/lib/matplotlib/bezier.py index 069e20d05916..42a6b478d729 100644 --- a/lib/matplotlib/bezier.py +++ b/lib/matplotlib/bezier.py @@ -54,7 +54,7 @@ def get_intersection(cx1, cy1, cos_t1, sin_t1, # rhs_inverse a_, b_ = d, -b c_, d_ = -c, a - a_, b_, c_, d_ = [k / ad_bc for k in [a_, b_, c_, d_]] + a_, b_, c_, d_ = (k / ad_bc for k in [a_, b_, c_, d_]) x = a_ * line1_rhs + b_ * line2_rhs y = c_ * line1_rhs + d_ * line2_rhs diff --git a/lib/matplotlib/category.py b/lib/matplotlib/category.py index 4ac2379ea5f5..225c837006f7 100644 --- a/lib/matplotlib/category.py +++ b/lib/matplotlib/category.py @@ -17,7 +17,7 @@ import numpy as np -from matplotlib import _api, ticker, units +from matplotlib import _api, cbook, ticker, units _log = logging.getLogger(__name__) @@ -55,7 +55,8 @@ def convert(value, unit, axis): values = np.atleast_1d(np.array(value, dtype=object)) # force an update so it also does type checking unit.update(values) - return np.vectorize(unit._mapping.__getitem__, otypes=[float])(values) + s = np.vectorize(unit._mapping.__getitem__, otypes=[float])(values) + return s if not cbook.is_scalar_or_string(value) else s[0] @staticmethod def axisinfo(unit, axis): @@ -227,7 +228,8 @@ def update(self, data): # Register the converter with Matplotlib's unit framework -units.registry[str] = StrCategoryConverter() -units.registry[np.str_] = StrCategoryConverter() -units.registry[bytes] = StrCategoryConverter() -units.registry[np.bytes_] = StrCategoryConverter() +# Intentionally set to a single instance +units.registry[str] = \ + units.registry[np.str_] = \ + units.registry[bytes] = \ + units.registry[np.bytes_] = StrCategoryConverter() diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c5b851ff6c9b..da7a122b0968 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -32,6 +32,29 @@ from matplotlib import _api, _c_internal_utils +class _ExceptionInfo: + """ + A class to carry exception information around. + + This is used to store and later raise exceptions. It's an alternative to + directly storing Exception instances that circumvents traceback-related + issues: caching tracebacks can keep user's objects in local namespaces + alive indefinitely, which can lead to very surprising memory issues for + users and result in incorrect tracebacks. + """ + + def __init__(self, cls, *args): + self._cls = cls + self._args = args + + @classmethod + def from_exception(cls, exc): + return cls(type(exc), *exc.args) + + def to_exception(self): + return self._cls(*self._args) + + def _get_running_interactive_framework(): """ Return the interactive framework whose event loop is currently running, if @@ -117,6 +140,61 @@ def _weak_or_strong_ref(func, callback): return _StrongRef(func) +class _UnhashDict: + """ + A minimal dict-like class that also supports unhashable keys, storing them + in a list of key-value pairs. + + This class only implements the interface needed for `CallbackRegistry`, and + tries to minimize the overhead for the hashable case. + """ + + def __init__(self, pairs): + self._dict = {} + self._pairs = [] + for k, v in pairs: + self[k] = v + + def __setitem__(self, key, value): + try: + self._dict[key] = value + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + self._pairs[i] = (key, value) + break + else: + self._pairs.append((key, value)) + + def __getitem__(self, key): + try: + return self._dict[key] + except TypeError: + pass + for k, v in self._pairs: + if k == key: + return v + raise KeyError(key) + + def pop(self, key, *args): + try: + if key in self._dict: + return self._dict.pop(key) + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + del self._pairs[i] + return v + if args: + return args[0] + raise KeyError(key) + + def __iter__(self): + yield from self._dict + for k, v in self._pairs: + yield k + + class CallbackRegistry: """ Handle registering, processing, blocking, and disconnecting @@ -176,14 +254,14 @@ class CallbackRegistry: # We maintain two mappings: # callbacks: signal -> {cid -> weakref-to-callback} - # _func_cid_map: signal -> {weakref-to-callback -> cid} + # _func_cid_map: {(signal, weakref-to-callback) -> cid} def __init__(self, exception_handler=_exception_printer, *, signals=None): self._signals = None if signals is None else list(signals) # Copy it. self.exception_handler = exception_handler self.callbacks = {} self._cid_gen = itertools.count() - self._func_cid_map = {} + self._func_cid_map = _UnhashDict([]) # A hidden variable that marks cids that need to be pickled. self._pickled_cids = set() @@ -204,27 +282,25 @@ def __setstate__(self, state): cid_count = state.pop('_cid_gen') vars(self).update(state) self.callbacks = { - s: {cid: _weak_or_strong_ref(func, self._remove_proxy) + s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s)) for cid, func in d.items()} for s, d in self.callbacks.items()} - self._func_cid_map = { - s: {proxy: cid for cid, proxy in d.items()} - for s, d in self.callbacks.items()} + self._func_cid_map = _UnhashDict( + ((s, proxy), cid) + for s, d in self.callbacks.items() for cid, proxy in d.items()) self._cid_gen = itertools.count(cid_count) def connect(self, signal, func): """Register *func* to be called when signal *signal* is generated.""" if self._signals is not None: _api.check_in_list(self._signals, signal=signal) - self._func_cid_map.setdefault(signal, {}) - proxy = _weak_or_strong_ref(func, self._remove_proxy) - if proxy in self._func_cid_map[signal]: - return self._func_cid_map[signal][proxy] - cid = next(self._cid_gen) - self._func_cid_map[signal][proxy] = cid - self.callbacks.setdefault(signal, {}) - self.callbacks[signal][cid] = proxy - return cid + proxy = _weak_or_strong_ref(func, functools.partial(self._remove_proxy, signal)) + try: + return self._func_cid_map[signal, proxy] + except KeyError: + cid = self._func_cid_map[signal, proxy] = next(self._cid_gen) + self.callbacks.setdefault(signal, {})[cid] = proxy + return cid def _connect_picklable(self, signal, func): """ @@ -238,23 +314,18 @@ def _connect_picklable(self, signal, func): # Keep a reference to sys.is_finalizing, as sys may have been cleared out # at that point. - def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing): + def _remove_proxy(self, signal, proxy, *, _is_finalizing=sys.is_finalizing): if _is_finalizing(): # Weakrefs can't be properly torn down at that point anymore. return - for signal, proxy_to_cid in list(self._func_cid_map.items()): - cid = proxy_to_cid.pop(proxy, None) - if cid is not None: - del self.callbacks[signal][cid] - self._pickled_cids.discard(cid) - break - else: - # Not found + cid = self._func_cid_map.pop((signal, proxy), None) + if cid is not None: + del self.callbacks[signal][cid] + self._pickled_cids.discard(cid) + else: # Not found return - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def disconnect(self, cid): """ @@ -263,24 +334,16 @@ def disconnect(self, cid): No error is raised if such a callback does not exist. """ self._pickled_cids.discard(cid) - # Clean up callbacks - for signal, cid_to_proxy in list(self.callbacks.items()): - proxy = cid_to_proxy.pop(cid, None) - if proxy is not None: + for signal, proxy in self._func_cid_map: + if self._func_cid_map[signal, proxy] == cid: break - else: - # Not found + else: # Not found return - - proxy_to_cid = self._func_cid_map[signal] - for current_proxy, current_cid in list(proxy_to_cid.items()): - if current_cid == cid: - assert proxy is current_proxy - del proxy_to_cid[current_proxy] - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + assert self.callbacks[signal][cid] == proxy + del self.callbacks[signal][cid] + self._func_cid_map.pop((signal, proxy)) + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def process(self, s, *args, **kwargs): """ @@ -503,9 +566,7 @@ def is_scalar_or_string(val): return isinstance(val, str) or not np.iterable(val) -@_api.delete_parameter( - "3.8", "np_load", alternative="open(get_sample_data(..., asfileobj=False))") -def get_sample_data(fname, asfileobj=True, *, np_load=True): +def get_sample_data(fname, asfileobj=True): """ Return a sample data file. *fname* is a path relative to the :file:`mpl-data/sample_data` directory. If *asfileobj* is `True` @@ -524,10 +585,7 @@ def get_sample_data(fname, asfileobj=True, *, np_load=True): if suffix == '.gz': return gzip.open(path) elif suffix in ['.npy', '.npz']: - if np_load: - return np.load(path) - else: - return path.open('rb') + return np.load(path) elif suffix in ['.csv', '.xrc', '.txt']: return path.open('r') else: @@ -557,9 +615,9 @@ def flatten(seq, scalarp=is_scalar_or_string): ['John', 'Hunter', 1, 23, 42, 5, 23] By: Composite of Holger Krekel and Luther Blissett - From: https://code.activestate.com/recipes/121294/ + From: https://code.activestate.com/recipes/121294-simple-generator-for-flattening-nested-containers/ and Recipe 1.12 in cookbook - """ + """ # noqa: E501 for item in seq: if scalarp(item) or item is None: yield item @@ -567,113 +625,6 @@ def flatten(seq, scalarp=is_scalar_or_string): yield from flatten(item, scalarp) -@_api.deprecated("3.8") -class Stack: - """ - Stack of elements with a movable cursor. - - Mimics home/back/forward in a web browser. - """ - - def __init__(self, default=None): - self.clear() - self._default = default - - def __call__(self): - """Return the current element, or None.""" - if not self._elements: - return self._default - else: - return self._elements[self._pos] - - def __len__(self): - return len(self._elements) - - def __getitem__(self, ind): - return self._elements[ind] - - def forward(self): - """Move the position forward and return the current element.""" - self._pos = min(self._pos + 1, len(self._elements) - 1) - return self() - - def back(self): - """Move the position back and return the current element.""" - if self._pos > 0: - self._pos -= 1 - return self() - - def push(self, o): - """ - Push *o* to the stack at current position. Discard all later elements. - - *o* is returned. - """ - self._elements = self._elements[:self._pos + 1] + [o] - self._pos = len(self._elements) - 1 - return self() - - def home(self): - """ - Push the first element onto the top of the stack. - - The first element is returned. - """ - if not self._elements: - return - self.push(self._elements[0]) - return self() - - def empty(self): - """Return whether the stack is empty.""" - return len(self._elements) == 0 - - def clear(self): - """Empty the stack.""" - self._pos = -1 - self._elements = [] - - def bubble(self, o): - """ - Raise all references of *o* to the top of the stack, and return it. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - top_elements = [] - for elem in old_elements: - if elem == o: - top_elements.append(elem) - else: - self.push(elem) - for _ in top_elements: - self.push(o) - return o - - def remove(self, o): - """ - Remove *o* from the stack. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - for elem in old_elements: - if elem != o: - self.push(elem) - - class _Stack: """ Stack of elements with a movable cursor. @@ -873,10 +824,6 @@ def __setstate__(self, state): def __contains__(self, item): return item in self._mapping - @_api.deprecated("3.8", alternative="none, you no longer need to clean a Grouper") - def clean(self): - """Clean dead weak references from the dictionary.""" - def join(self, a, *args): """ Join given arguments into the same set. Accepts one or more arguments. @@ -2377,6 +2324,21 @@ def _is_jax_array(x): return False +def _is_tensorflow_array(x): + """Check if 'x' is a TensorFlow Tensor or Variable.""" + try: + # we're intentionally not attempting to import TensorFlow. If somebody + # has created a TensorFlow array, TensorFlow should already be in sys.modules + # we use `is_tensor` to not depend on the class structure of TensorFlow + # arrays, as `tf.Variables` are not instances of `tf.Tensor` + # (they both convert the same way) + return isinstance(x, sys.modules['tensorflow'].is_tensor(x)) + except Exception: # TypeError, KeyError, AttributeError, maybe others? + # we're attempting to access attributes on imported modules which + # may have arbitrary user code, so we deliberately catch all exceptions + return False + + def _unpack_to_numpy(x): """Internal helper to extract data from e.g. pandas and xarray objects.""" if isinstance(x, np.ndarray): @@ -2391,10 +2353,14 @@ def _unpack_to_numpy(x): # so in this case we do not want to return a function if isinstance(xtmp, np.ndarray): return xtmp - if _is_torch_array(x) or _is_jax_array(x): - xtmp = x.__array__() - - # In case __array__() method does not return a numpy array in future + if _is_torch_array(x) or _is_jax_array(x) or _is_tensorflow_array(x): + # using np.asarray() instead of explicitly __array__(), as the latter is + # only _one_ of many methods, and it's the last resort, see also + # https://numpy.org/devdocs/user/basics.interoperability.html#using-arbitrary-objects-in-numpy + # therefore, let arrays do better if they can + xtmp = np.asarray(x) + + # In case np.asarray method does not return a numpy array in future if isinstance(xtmp, np.ndarray): return xtmp return x @@ -2425,3 +2391,15 @@ def _auto_format_str(fmt, value): return fmt % (value,) except (TypeError, ValueError): return fmt.format(value) + + +def _is_pandas_dataframe(x): + """Check if 'x' is a Pandas DataFrame.""" + try: + # we're intentionally not attempting to import Pandas. If somebody + # has created a Pandas DataFrame, Pandas should already be in sys.modules + return isinstance(x, sys.modules['pandas'].DataFrame) + except Exception: # TypeError, KeyError, AttributeError, maybe others? + # we're attempting to access attributes on imported modules which + # may have arbitrary user code, so we deliberately catch all exceptions + return False diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index d727b8065b7a..cc6b4e8f4e19 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -74,26 +74,18 @@ def open_file_cm( def is_scalar_or_string(val: Any) -> bool: ... @overload def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True] -) -> np.ndarray: ... + fname: str | os.PathLike, asfileobj: Literal[True] = ... +) -> np.ndarray | IO: ... @overload -def get_sample_data( - fname: str | os.PathLike, - asfileobj: Literal[True] = ..., - *, - np_load: Literal[False] = ..., -) -> IO: ... -@overload -def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[False], *, np_load: bool = ... -) -> str: ... +def get_sample_data(fname: str | os.PathLike, asfileobj: Literal[False]) -> str: ... def _get_data_path(*args: Path | str) -> Path: ... def flatten( seq: Iterable[Any], scalarp: Callable[[Any], bool] = ... ) -> Generator[Any, None, None]: ... -class Stack(Generic[_T]): - def __init__(self, default: _T | None = ...) -> None: ... +class _Stack(Generic[_T]): + def __init__(self) -> None: ... + def clear(self) -> None: ... def __call__(self) -> _T: ... def __len__(self) -> int: ... def __getitem__(self, ind: int) -> _T: ... @@ -101,10 +93,6 @@ class Stack(Generic[_T]): def back(self) -> _T: ... def push(self, o: _T) -> _T: ... def home(self) -> _T: ... - def empty(self) -> bool: ... - def clear(self) -> None: ... - def bubble(self, o: _T) -> _T: ... - def remove(self, o: _T) -> None: ... def safe_masked_invalid(x: ArrayLike, copy: bool = ...) -> np.ndarray: ... def print_cycles( @@ -114,7 +102,6 @@ def print_cycles( class Grouper(Generic[_T]): def __init__(self, init: Iterable[_T] = ...) -> None: ... def __contains__(self, item: _T) -> bool: ... - def clean(self) -> None: ... def join(self, a: _T, *args: _T) -> None: ... def joined(self, a: _T, b: _T) -> bool: ... def remove(self, a: _T) -> None: ... diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index b0cb3e9a7ec1..0c11527bc2b9 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -15,15 +15,15 @@ """ from collections.abc import Mapping -import functools - -import numpy as np -from numpy import ma import matplotlib as mpl -from matplotlib import _api, colors, cbook, scale +from matplotlib import _api, colors +# TODO make this warn on access +from matplotlib.colorizer import _ScalarMappable as ScalarMappable # noqa from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed +from matplotlib._cm_multivar import cmap_families as multivar_cmaps +from matplotlib._cm_bivar import cmaps as bivar_cmaps _LUTSIZE = mpl.rcParams['image.lut'] @@ -44,10 +44,17 @@ def _gen_cmap_registry(): colors.LinearSegmentedColormap.from_list(name, spec, _LUTSIZE)) # Register colormap aliases for gray and grey. - cmap_d['grey'] = cmap_d['gray'] - cmap_d['gist_grey'] = cmap_d['gist_gray'] - cmap_d['gist_yerg'] = cmap_d['gist_yarg'] - cmap_d['Grays'] = cmap_d['Greys'] + aliases = { + # alias -> original name + 'grey': 'gray', + 'gist_grey': 'gist_gray', + 'gist_yerg': 'gist_yarg', + 'Grays': 'Greys', + } + for alias, original_name in aliases.items(): + cmap = cmap_d[original_name].copy() + cmap.name = alias + cmap_d[alias] = cmap # Generate reversed cmaps. for cmap in list(cmap_d.values()): @@ -231,6 +238,10 @@ def get_cmap(self, cmap): _colormaps = ColormapRegistry(_gen_cmap_registry()) globals().update(_colormaps) +_multivar_colormaps = ColormapRegistry(multivar_cmaps) + +_bivar_colormaps = ColormapRegistry(bivar_cmaps) + # This is an exact copy of pyplot.get_cmap(). It was removed in 3.9, but apparently # caused more user trouble than expected. Re-added for 3.9.1 and extended the @@ -270,371 +281,6 @@ def get_cmap(name=None, lut=None): return _colormaps[name].resampled(lut) -def _auto_norm_from_scale(scale_cls): - """ - Automatically generate a norm class from *scale_cls*. - - This differs from `.colors.make_norm_from_scale` in the following points: - - - This function is not a class decorator, but directly returns a norm class - (as if decorating `.Normalize`). - - The scale is automatically constructed with ``nonpositive="mask"``, if it - supports such a parameter, to work around the difference in defaults - between standard scales (which use "clip") and norms (which use "mask"). - - Note that ``make_norm_from_scale`` caches the generated norm classes - (not the instances) and reuses them for later calls. For example, - ``type(_auto_norm_from_scale("log")) == LogNorm``. - """ - # Actually try to construct an instance, to verify whether - # ``nonpositive="mask"`` is supported. - try: - norm = colors.make_norm_from_scale( - functools.partial(scale_cls, nonpositive="mask"))( - colors.Normalize)() - except TypeError: - norm = colors.make_norm_from_scale(scale_cls)( - colors.Normalize)() - return type(norm) - - -class ScalarMappable: - """ - A mixin class to map scalar data to RGBA. - - The ScalarMappable applies data normalization before returning RGBA colors - from the given colormap. - """ - - def __init__(self, norm=None, cmap=None): - """ - Parameters - ---------- - norm : `.Normalize` (or subclass thereof) or str or None - The normalizing object which scales data, typically into the - interval ``[0, 1]``. - If a `str`, a `.Normalize` subclass is dynamically generated based - on the scale with the corresponding name. - If *None*, *norm* defaults to a *colors.Normalize* object which - initializes its scaling based on the first data processed. - cmap : str or `~matplotlib.colors.Colormap` - The colormap used to map normalized data values to RGBA colors. - """ - self._A = None - self._norm = None # So that the setter knows we're initializing. - self.set_norm(norm) # The Normalize instance of this ScalarMappable. - self.cmap = None # So that the setter knows we're initializing. - self.set_cmap(cmap) # The Colormap instance of this ScalarMappable. - #: The last colorbar associated with this ScalarMappable. May be None. - self.colorbar = None - self.callbacks = cbook.CallbackRegistry(signals=["changed"]) - - def _scale_norm(self, norm, vmin, vmax): - """ - Helper for initial scaling. - - Used by public functions that create a ScalarMappable and support - parameters *vmin*, *vmax* and *norm*. This makes sure that a *norm* - will take precedence over *vmin*, *vmax*. - - Note that this method does not set the norm. - """ - if vmin is not None or vmax is not None: - self.set_clim(vmin, vmax) - if isinstance(norm, colors.Normalize): - raise ValueError( - "Passing a Normalize instance simultaneously with " - "vmin/vmax is not supported. Please pass vmin/vmax " - "directly to the norm when creating it.") - - # always resolve the autoscaling so we have concrete limits - # rather than deferring to draw time. - self.autoscale_None() - - def to_rgba(self, x, alpha=None, bytes=False, norm=True): - """ - Return a normalized RGBA array corresponding to *x*. - - In the normal case, *x* is a 1D or 2D sequence of scalars, and - the corresponding `~numpy.ndarray` of RGBA values will be returned, - based on the norm and colormap set for this ScalarMappable. - - There is one special case, for handling images that are already - RGB or RGBA, such as might have been read from an image file. - If *x* is an `~numpy.ndarray` with 3 dimensions, - and the last dimension is either 3 or 4, then it will be - treated as an RGB or RGBA array, and no mapping will be done. - The array can be `~numpy.uint8`, or it can be floats with - values in the 0-1 range; otherwise a ValueError will be raised. - Any NaNs or masked elements will be set to 0 alpha. - If the last dimension is 3, the *alpha* kwarg (defaulting to 1) - will be used to fill in the transparency. If the last dimension - is 4, the *alpha* kwarg is ignored; it does not - replace the preexisting alpha. A ValueError will be raised - if the third dimension is other than 3 or 4. - - In either case, if *bytes* is *False* (default), the RGBA - array will be floats in the 0-1 range; if it is *True*, - the returned RGBA array will be `~numpy.uint8` in the 0 to 255 range. - - If norm is False, no normalization of the input data is - performed, and it is assumed to be in the range (0-1). - - """ - # First check for special case, image input: - try: - if x.ndim == 3: - if x.shape[2] == 3: - if alpha is None: - alpha = 1 - if x.dtype == np.uint8: - alpha = np.uint8(alpha * 255) - m, n = x.shape[:2] - xx = np.empty(shape=(m, n, 4), dtype=x.dtype) - xx[:, :, :3] = x - xx[:, :, 3] = alpha - elif x.shape[2] == 4: - xx = x - else: - raise ValueError("Third dimension must be 3 or 4") - if xx.dtype.kind == 'f': - # If any of R, G, B, or A is nan, set to 0 - if np.any(nans := np.isnan(x)): - if x.shape[2] == 4: - xx = xx.copy() - xx[np.any(nans, axis=2), :] = 0 - - if norm and (xx.max() > 1 or xx.min() < 0): - raise ValueError("Floating point image RGB values " - "must be in the 0..1 range.") - if bytes: - xx = (xx * 255).astype(np.uint8) - elif xx.dtype == np.uint8: - if not bytes: - xx = xx.astype(np.float32) / 255 - else: - raise ValueError("Image RGB array must be uint8 or " - "floating point; found %s" % xx.dtype) - # Account for any masked entries in the original array - # If any of R, G, B, or A are masked for an entry, we set alpha to 0 - if np.ma.is_masked(x): - xx[np.any(np.ma.getmaskarray(x), axis=2), 3] = 0 - return xx - except AttributeError: - # e.g., x is not an ndarray; so try mapping it - pass - - # This is the normal case, mapping a scalar array: - x = ma.asarray(x) - if norm: - x = self.norm(x) - rgba = self.cmap(x, alpha=alpha, bytes=bytes) - return rgba - - def set_array(self, A): - """ - Set the value array from array-like *A*. - - Parameters - ---------- - A : array-like or None - The values that are mapped to colors. - - The base class `.ScalarMappable` does not make any assumptions on - the dimensionality and shape of the value array *A*. - """ - if A is None: - self._A = None - return - - 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") - - self._A = A - if not self.norm.scaled(): - self.norm.autoscale_None(A) - - def get_array(self): - """ - Return the array of values, that are mapped to colors. - - The base class `.ScalarMappable` does not make any assumptions on - the dimensionality and shape of the array. - """ - return self._A - - def get_cmap(self): - """Return the `.Colormap` instance.""" - return self.cmap - - def get_clim(self): - """ - Return the values (min, max) that are mapped to the colormap limits. - """ - return self.norm.vmin, self.norm.vmax - - def set_clim(self, vmin=None, vmax=None): - """ - Set the norm limits for image scaling. - - Parameters - ---------- - vmin, vmax : float - The limits. - - The limits may also be passed as a tuple (*vmin*, *vmax*) as a - 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 - if vmax is None: - try: - vmin, vmax = vmin - except (TypeError, ValueError): - pass - if vmin is not None: - self.norm.vmin = colors._sanitize_extrema(vmin) - if vmax is not None: - self.norm.vmax = colors._sanitize_extrema(vmax) - - def get_alpha(self): - """ - Returns - ------- - float - Always returns 1. - """ - # This method is intended to be overridden by Artist sub-classes - return 1. - - def set_cmap(self, cmap): - """ - Set the colormap for luminance data. - - Parameters - ---------- - cmap : `.Colormap` or str or None - """ - in_init = self.cmap is None - - self.cmap = _ensure_cmap(cmap) - if not in_init: - self.changed() # Things are not set up properly yet. - - @property - def norm(self): - return self._norm - - @norm.setter - def norm(self, norm): - _api.check_isinstance((colors.Normalize, 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)() - - if norm is self.norm: - # We aren't updating anything - return - - in_init = self.norm is None - # Remove the current callback and connect to the new one - if not in_init: - self.norm.callbacks.disconnect(self._id_norm) - self._norm = norm - self._id_norm = self.norm.callbacks.connect('changed', - self.changed) - if not in_init: - self.changed() - - def set_norm(self, norm): - """ - Set the normalization instance. - - Parameters - ---------- - norm : `.Normalize` or str or None - - Notes - ----- - If there are any colorbars using the mappable for this norm, setting - the norm of the mappable will reset the norm, locator, and formatters - on the colorbar to default. - """ - self.norm = norm - - def autoscale(self): - """ - Autoscale the scalar limits on the norm instance using the - current array - """ - if self._A is None: - raise TypeError('You must first set_array for mappable') - # If the norm's limits are updated self.changed() will be called - # through the callbacks attached to the norm - self.norm.autoscale(self._A) - - def autoscale_None(self): - """ - Autoscale the scalar limits on the norm instance using the - current array, changing only limits that are None - """ - if self._A is None: - raise TypeError('You must first set_array for mappable') - # If the norm's limits are updated self.changed() will be called - # through the callbacks attached to the norm - self.norm.autoscale_None(self._A) - - def changed(self): - """ - Call this whenever the mappable is changed to notify all the - callbackSM listeners to the 'changed' signal. - """ - self.callbacks.process('changed', self) - self.stale = True - - -# The docstrings here must be generic enough to apply to all relevant methods. -mpl._docstring.interpd.update( - cmap_doc="""\ -cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The Colormap instance or registered colormap name used to map scalar data - to colors.""", - norm_doc="""\ -norm : str or `~matplotlib.colors.Normalize`, optional - The normalization method used to scale scalar 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. - - If given, 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 that case, a suitable `.Normalize` subclass is dynamically generated - and instantiated.""", - vmin_vmax_doc="""\ -vmin, vmax : float, 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).""", -) - - def _ensure_cmap(cmap): """ Ensure that we have a `.Colormap` object. diff --git a/lib/matplotlib/cm.pyi b/lib/matplotlib/cm.pyi index be8f10b39cb6..c3c62095684a 100644 --- a/lib/matplotlib/cm.pyi +++ b/lib/matplotlib/cm.pyi @@ -1,9 +1,7 @@ from collections.abc import Iterator, Mapping -from matplotlib import cbook, colors -from matplotlib.colorbar import Colorbar +from matplotlib import colors +from matplotlib.colorizer import _ScalarMappable -import numpy as np -from numpy.typing import ArrayLike class ColormapRegistry(Mapping[str, colors.Colormap]): def __init__(self, cmaps: Mapping[str, colors.Colormap]) -> None: ... @@ -18,37 +16,9 @@ class ColormapRegistry(Mapping[str, colors.Colormap]): def get_cmap(self, cmap: str | colors.Colormap) -> colors.Colormap: ... _colormaps: ColormapRegistry = ... +_multivar_colormaps: ColormapRegistry = ... +_bivar_colormaps: ColormapRegistry = ... def get_cmap(name: str | colors.Colormap | None = ..., lut: int | None = ...) -> colors.Colormap: ... -class ScalarMappable: - cmap: colors.Colormap | None - colorbar: Colorbar | None - callbacks: cbook.CallbackRegistry - def __init__( - self, - norm: colors.Normalize | None = ..., - cmap: str | colors.Colormap | None = ..., - ) -> None: ... - def to_rgba( - self, - x: np.ndarray, - alpha: float | ArrayLike | None = ..., - bytes: bool = ..., - norm: bool = ..., - ) -> np.ndarray: ... - def set_array(self, A: ArrayLike | None) -> None: ... - def get_array(self) -> np.ndarray | None: ... - def get_cmap(self) -> colors.Colormap: ... - def get_clim(self) -> tuple[float, float]: ... - def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... - def get_alpha(self) -> float | None: ... - def set_cmap(self, cmap: str | colors.Colormap) -> None: ... - @property - def norm(self) -> colors.Normalize: ... - @norm.setter - def norm(self, norm: colors.Normalize | str | None) -> None: ... - def set_norm(self, norm: colors.Normalize | str | None) -> None: ... - def autoscale(self) -> None: ... - def autoscale_None(self) -> None: ... - def changed(self) -> None: ... +ScalarMappable = _ScalarMappable diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index fd6cc4339d64..f18d5a4c3a8c 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -10,6 +10,7 @@ """ import itertools +import functools import math from numbers import Number, Real import warnings @@ -17,8 +18,8 @@ import numpy as np import matplotlib as mpl -from . import (_api, _path, artist, cbook, cm, colors as mcolors, _docstring, - hatch as mhatch, lines as mlines, path as mpath, transforms) +from . import (_api, _path, artist, cbook, colorizer as mcolorizer, colors as mcolors, + _docstring, hatch as mhatch, lines as mlines, path as mpath, transforms) from ._enums import JoinStyle, CapStyle @@ -32,7 +33,7 @@ "linewidth": ["linewidths", "lw"], "offset_transform": ["transOffset"], }) -class Collection(artist.Artist, cm.ScalarMappable): +class Collection(mcolorizer.ColorizingArtist): r""" Base class for Collections. Must be subclassed to be usable. @@ -87,6 +88,7 @@ def __init__(self, *, offset_transform=None, norm=None, # optional for ScalarMappable cmap=None, # ditto + colorizer=None, pickradius=5.0, hatch=None, urls=None, @@ -113,10 +115,10 @@ def __init__(self, *, where *onoffseq* is an even length tuple of on and off ink lengths in points. For examples, see :doc:`/gallery/lines_bars_and_markers/linestyles`. - capstyle : `.CapStyle`-like, default: :rc:`patch.capstyle` + capstyle : `.CapStyle`-like, default: 'butt' Style to use for capping lines for all paths in the collection. Allowed values are %(CapStyle)s. - joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle` + joinstyle : `.JoinStyle`-like, default: 'round' Style to use for joining lines for all paths in the collection. Allowed values are %(JoinStyle)s. antialiaseds : bool or list of bool, default: :rc:`patch.antialiased` @@ -155,8 +157,8 @@ def __init__(self, *, Remaining keyword arguments will be used to set properties as ``Collection.set_{key}(val)`` for each key-value pair in *kwargs*. """ - artist.Artist.__init__(self) - cm.ScalarMappable.__init__(self, norm, cmap) + + super().__init__(self._get_colorizer(cmap, norm, colorizer)) # list of un-scaled dash patterns # this is needed scaling the dash pattern by linewidth self._us_linestyles = [(0, None)] @@ -173,6 +175,7 @@ def __init__(self, *, self._edge_is_mapped = None self._mapped_colors = None # calculated in update_scalarmappable self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color']) + self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self.set_facecolor(facecolors) self.set_edgecolor(edgecolors) self.set_linewidth(linewidths) @@ -363,6 +366,7 @@ def draw(self, renderer): if self._hatch: gc.set_hatch(self._hatch) gc.set_hatch_color(self._hatch_color) + gc.set_hatch_linewidth(self._hatch_linewidth) if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) @@ -392,8 +396,8 @@ def draw(self, renderer): else: combined_transform = transform extents = paths[0].get_extents(combined_transform) - if (extents.width < self.figure.bbox.width - and extents.height < self.figure.bbox.height): + if (extents.width < self.get_figure(root=True).bbox.width + and extents.height < self.get_figure(root=True).bbox.height): do_single_path_optimization = True if self._joinstyle: @@ -541,6 +545,14 @@ def get_hatch(self): """Return the current hatching pattern.""" return self._hatch + def set_hatch_linewidth(self, lw): + """Set the hatch linewidth.""" + self._hatch_linewidth = lw + + def get_hatch_linewidth(self): + """Return the hatch linewidth.""" + return self._hatch_linewidth + def set_offsets(self, offsets): """ Set the offsets for the collection. @@ -1001,7 +1013,7 @@ def set_sizes(self, sizes, dpi=72.0): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) + self.set_sizes(self._sizes, self.get_figure(root=True).dpi) super().draw(renderer) @@ -1254,6 +1266,248 @@ def set_verts_and_codes(self, verts, codes): self.stale = True +class FillBetweenPolyCollection(PolyCollection): + """ + `.PolyCollection` that fills the area between two x- or y-curves. + """ + def __init__( + self, t_direction, t, f1, f2, *, + where=None, interpolate=False, step=None, **kwargs): + """ + Parameters + ---------- + t_direction : {{'x', 'y'}} + The axes on which the variable lies. + + - 'x': the curves are ``(t, f1)`` and ``(t, f2)``. + - 'y': the curves are ``(f1, t)`` and ``(f2, t)``. + + t : array (length N) + The ``t_direction`` coordinates of the nodes defining the curves. + + f1 : array (length N) or scalar + The other coordinates of the nodes defining the first curve. + + f2 : array (length N) or scalar + The other coordinates of the nodes defining the second curve. + + where : array of bool (length N), optional + Define *where* to exclude some {dir} regions from being filled. + The filled regions are defined by the coordinates ``t[where]``. + More precisely, fill between ``t[i]`` and ``t[i+1]`` if + ``where[i] and where[i+1]``. Note that this definition implies + that an isolated *True* value between two *False* values in *where* + will not result in filling. Both sides of the *True* position + remain unfilled due to the adjacent *False* values. + + interpolate : bool, default: False + This option is only relevant if *where* is used and the two curves + are crossing each other. + + Semantically, *where* is often used for *f1* > *f2* or + similar. By default, the nodes of the polygon defining the filled + region will only be placed at the positions in the *t* array. + Such a polygon cannot describe the above semantics close to the + intersection. The t-sections containing the intersection are + simply clipped. + + Setting *interpolate* to *True* will calculate the actual + intersection point and extend the filled region up to this point. + + step : {{'pre', 'post', 'mid'}}, optional + Define *step* if the filling should be a step function, + i.e. constant in between *t*. The value determines where the + step will occur: + + - 'pre': The f value is continued constantly to the left from + every *t* position, i.e. the interval ``(t[i-1], t[i]]`` has the + value ``f[i]``. + - 'post': The y value is continued constantly to the right from + every *x* position, i.e. the interval ``[t[i], t[i+1])`` has the + value ``f[i]``. + - 'mid': Steps occur half-way between the *t* positions. + + **kwargs + Forwarded to `.PolyCollection`. + + See Also + -------- + .Axes.fill_between, .Axes.fill_betweenx + """ + self.t_direction = t_direction + self._interpolate = interpolate + self._step = step + verts = self._make_verts(t, f1, f2, where) + super().__init__(verts, **kwargs) + + @staticmethod + def _f_dir_from_t(t_direction): + """The direction that is other than `t_direction`.""" + if t_direction == "x": + return "y" + elif t_direction == "y": + return "x" + else: + msg = f"t_direction must be 'x' or 'y', got {t_direction!r}" + raise ValueError(msg) + + @property + def _f_direction(self): + """The direction that is other than `self.t_direction`.""" + return self._f_dir_from_t(self.t_direction) + + def set_data(self, t, f1, f2, *, where=None): + """ + Set new values for the two bounding curves. + + Parameters + ---------- + t : array (length N) + The ``self.t_direction`` coordinates of the nodes defining the curves. + + f1 : array (length N) or scalar + The other coordinates of the nodes defining the first curve. + + f2 : array (length N) or scalar + The other coordinates of the nodes defining the second curve. + + where : array of bool (length N), optional + Define *where* to exclude some {dir} regions from being filled. + The filled regions are defined by the coordinates ``t[where]``. + More precisely, fill between ``t[i]`` and ``t[i+1]`` if + ``where[i] and where[i+1]``. Note that this definition implies + that an isolated *True* value between two *False* values in *where* + will not result in filling. Both sides of the *True* position + remain unfilled due to the adjacent *False* values. + + See Also + -------- + .PolyCollection.set_verts, .Line2D.set_data + """ + t, f1, f2 = self.axes._fill_between_process_units( + self.t_direction, self._f_direction, t, f1, f2) + + verts = self._make_verts(t, f1, f2, where) + self.set_verts(verts) + + def get_datalim(self, transData): + """Calculate the data limits and return them as a `.Bbox`.""" + datalim = transforms.Bbox.null() + datalim.update_from_data_xy((self.get_transform() - transData).transform( + np.concatenate([self._bbox, [self._bbox.minpos]]))) + return datalim + + def _make_verts(self, t, f1, f2, where): + """ + Make verts that can be forwarded to `.PolyCollection`. + """ + self._validate_shapes(self.t_direction, self._f_direction, t, f1, f2) + + where = self._get_data_mask(t, f1, f2, where) + t, f1, f2 = np.broadcast_arrays(np.atleast_1d(t), f1, f2, subok=True) + + self._bbox = transforms.Bbox.null() + self._bbox.update_from_data_xy(self._fix_pts_xy_order(np.concatenate([ + np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)]))) + + return [ + self._make_verts_for_region(t, f1, f2, idx0, idx1) + for idx0, idx1 in cbook.contiguous_regions(where) + ] + + def _get_data_mask(self, t, f1, f2, where): + """ + Return a bool array, with True at all points that should eventually be rendered. + + The array is True at a point if none of the data inputs + *t*, *f1*, *f2* is masked and if the input *where* is true at that point. + """ + if where is None: + where = True + else: + where = np.asarray(where, dtype=bool) + if where.size != t.size: + msg = "where size ({}) does not match {!r} size ({})".format( + where.size, self.t_direction, t.size) + raise ValueError(msg) + return where & ~functools.reduce( + np.logical_or, map(np.ma.getmaskarray, [t, f1, f2])) + + @staticmethod + def _validate_shapes(t_dir, f_dir, t, f1, f2): + """Validate that t, f1 and f2 are 1-dimensional and have the same length.""" + names = (d + s for d, s in zip((t_dir, f_dir, f_dir), ("", "1", "2"))) + for name, array in zip(names, [t, f1, f2]): + if array.ndim > 1: + raise ValueError(f"{name!r} is not 1-dimensional") + if t.size > 1 and array.size > 1 and t.size != array.size: + msg = "{!r} has size {}, but {!r} has an unequal size of {}".format( + t_dir, t.size, name, array.size) + raise ValueError(msg) + + def _make_verts_for_region(self, t, f1, f2, idx0, idx1): + """ + Make ``verts`` for a contiguous region between ``idx0`` and ``idx1``, taking + into account ``step`` and ``interpolate``. + """ + t_slice = t[idx0:idx1] + f1_slice = f1[idx0:idx1] + f2_slice = f2[idx0:idx1] + if self._step is not None: + step_func = cbook.STEP_LOOKUP_MAP["steps-" + self._step] + t_slice, f1_slice, f2_slice = step_func(t_slice, f1_slice, f2_slice) + + if self._interpolate: + start = self._get_interpolating_points(t, f1, f2, idx0) + end = self._get_interpolating_points(t, f1, f2, idx1) + else: + # Handle scalar f2 (e.g. 0): the fill should go all + # the way down to 0 even if none of the dep1 sample points do. + start = t_slice[0], f2_slice[0] + end = t_slice[-1], f2_slice[-1] + + pts = np.concatenate(( + np.asarray([start]), + np.stack((t_slice, f1_slice), axis=-1), + np.asarray([end]), + np.stack((t_slice, f2_slice), axis=-1)[::-1])) + + return self._fix_pts_xy_order(pts) + + @classmethod + def _get_interpolating_points(cls, t, f1, f2, idx): + """Calculate interpolating points.""" + im1 = max(idx - 1, 0) + t_values = t[im1:idx+1] + diff_values = f1[im1:idx+1] - f2[im1:idx+1] + f1_values = f1[im1:idx+1] + + if len(diff_values) == 2: + if np.ma.is_masked(diff_values[1]): + return t[im1], f1[im1] + elif np.ma.is_masked(diff_values[0]): + return t[idx], f1[idx] + + diff_root_t = cls._get_diff_root(0, diff_values, t_values) + diff_root_f = cls._get_diff_root(diff_root_t, t_values, f1_values) + return diff_root_t, diff_root_f + + @staticmethod + def _get_diff_root(x, xp, fp): + """Calculate diff root.""" + order = xp.argsort() + return np.interp(x, xp[order], fp[order]) + + def _fix_pts_xy_order(self, pts): + """ + Fix pts calculation results with `self.t_direction`. + + In the workflow, it is assumed that `self.t_direction` is 'x'. If this + is not true, we need to exchange the coordinates. + """ + return pts[:, ::-1] if self.t_direction == "y" else pts + + class RegularPolyCollection(_CollectionWithSizes): """A collection of n-sided regular polygons.""" @@ -1310,7 +1564,7 @@ def get_rotation(self): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) + self.set_sizes(self._sizes, self.get_figure(root=True).dpi) self._transforms = [ transforms.Affine2D(x).rotate(-self._rotation).get_matrix() for x in self._transforms @@ -1358,14 +1612,13 @@ def __init__(self, segments, # Can be None. """ Parameters ---------- - segments : list of array-like - A sequence (*line0*, *line1*, *line2*) of lines, where each line is a list - of points:: + segments : list of (N, 2) array-like + A sequence ``[line0, line1, ...]`` where each line is a (N, 2)-shape + array-like containing points:: - lineN = [(x0, y0), (x1, y1), ... (xm, ym)] + line0 = [(x0, y0), (x1, y1), ...] - or the equivalent Mx2 numpy array with two columns. Each line - can have a different number of segments. + Each line can contain a different number of points. linewidths : float or list of float, default: :rc:`lines.linewidth` The width of each line in points. colors : :mpltype:`color` or list of color, default: :rc:`lines.color` @@ -1757,7 +2010,7 @@ def _set_transforms(self): """Calculate transforms immediately before drawing.""" ax = self.axes - fig = self.figure + fig = self.get_figure(root=False) if self._units == 'xy': sc = 1 @@ -2252,14 +2505,8 @@ class PolyQuadMesh(_MeshData, PolyCollection): """ def __init__(self, coordinates, **kwargs): - # We need to keep track of whether we are using deprecated compression - # Update it after the initializers - self._deprecated_compression = False super().__init__(coordinates=coordinates) PolyCollection.__init__(self, verts=[], **kwargs) - # Store this during the compression deprecation period - self._original_mask = ~self._get_unmasked_polys() - self._deprecated_compression = np.any(self._original_mask) # Setting the verts updates the paths of the PolyCollection # This is called after the initializers to make sure the kwargs # have all been processed and available for the masking calculations @@ -2272,14 +2519,7 @@ def _get_unmasked_polys(self): # We want the shape of the polygon, which is the corner of each X/Y array mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) - - if (getattr(self, "_deprecated_compression", False) and - np.any(self._original_mask)): - return ~(mask | self._original_mask) - # Take account of the array data too, temporarily avoiding - # the compression warning and resetting the variable after the call - with cbook._setattr_cm(self, _deprecated_compression=False): - arr = self.get_array() + arr = self.get_array() if arr is not None: arr = np.ma.getmaskarray(arr) if arr.ndim == 3: @@ -2335,42 +2575,8 @@ def get_facecolor(self): def set_array(self, A): # docstring inherited prev_unmask = self._get_unmasked_polys() - # MPL <3.8 compressed the mask, so we need to handle flattened 1d input - # until the deprecation expires, also only warning when there are masked - # elements and thus compression occurring. - if self._deprecated_compression and np.ndim(A) == 1: - _api.warn_deprecated("3.8", message="Setting a PolyQuadMesh array using " - "the compressed values is deprecated. " - "Pass the full 2D shape of the original array " - f"{prev_unmask.shape} including the masked elements.") - Afull = np.empty(self._original_mask.shape) - Afull[~self._original_mask] = A - # We also want to update the mask with any potential - # new masked elements that came in. But, we don't want - # to update any of the compression from the original - mask = self._original_mask.copy() - mask[~self._original_mask] |= np.ma.getmask(A) - A = np.ma.array(Afull, mask=mask) - return super().set_array(A) - self._deprecated_compression = False super().set_array(A) # If the mask has changed at all we need to update # the set of Polys that we are drawing if not np.array_equal(prev_unmask, self._get_unmasked_polys()): self._set_unmasked_verts() - - def get_array(self): - # docstring inherited - # Can remove this entire function once the deprecation period ends - A = super().get_array() - if A is None: - return - if self._deprecated_compression and np.any(np.ma.getmask(A)): - _api.warn_deprecated("3.8", message=( - "Getting the array from a PolyQuadMesh will return the full " - "array in the future (uncompressed). To get this behavior now " - "set the PolyQuadMesh with a 2D array .set_array(data2d).")) - # Setting an array of a polycollection required - # compressing the array - return np.ma.compressed(A) - return A diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index e4c46229517f..0805adef4293 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -4,7 +4,7 @@ from typing import Literal import numpy as np from numpy.typing import ArrayLike, NDArray -from . import artist, cm, transforms +from . import colorizer, transforms from .backend_bases import MouseEvent from .artist import Artist from .colors import Normalize, Colormap @@ -15,7 +15,7 @@ from .ticker import Locator, Formatter from .tri import Triangulation from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType -class Collection(artist.Artist, cm.ScalarMappable): +class Collection(colorizer.ColorizingArtist): def __init__( self, *, @@ -30,6 +30,7 @@ class Collection(artist.Artist, cm.ScalarMappable): offset_transform: transforms.Transform | None = ..., norm: Normalize | None = ..., cmap: Colormap | None = ..., + colorizer: colorizer.Colorizer | None = ..., pickradius: float = ..., hatch: str | None = ..., urls: Sequence[str] | None = ..., @@ -48,6 +49,8 @@ class Collection(artist.Artist, cm.ScalarMappable): def get_urls(self) -> Sequence[str | None]: ... def set_hatch(self, hatch: str) -> None: ... def get_hatch(self) -> str: ... + def set_hatch_linewidth(self, lw: float) -> None: ... + def get_hatch_linewidth(self) -> float: ... def set_offsets(self, offsets: ArrayLike) -> None: ... def get_offsets(self) -> ArrayLike: ... def set_linewidth(self, lw: float | Sequence[float]) -> None: ... @@ -106,6 +109,29 @@ class PolyCollection(_CollectionWithSizes): self, verts: Sequence[ArrayLike | Path], codes: Sequence[int] ) -> None: ... +class FillBetweenPolyCollection(PolyCollection): + def __init__( + self, + t_direction: Literal["x", "y"], + t: ArrayLike, + f1: ArrayLike, + f2: ArrayLike, + *, + where: Sequence[bool] | None = ..., + interpolate: bool = ..., + step: Literal["pre", "post", "mid"] | None = ..., + **kwargs, + ) -> None: ... + def set_data( + self, + t: ArrayLike, + f1: ArrayLike, + f2: ArrayLike, + *, + where: Sequence[bool] | None = ..., + ) -> None: ... + def get_datalim(self, transData: transforms.Transform) -> transforms.Bbox: ... + class RegularPolyCollection(_CollectionWithSizes): def __init__( self, numsides: int, *, rotation: float = ..., sizes: ArrayLike = ..., **kwargs diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 156ea2ff6497..51ada59a21a0 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -26,7 +26,7 @@ _log = logging.getLogger(__name__) -_docstring.interpd.update( +_docstring.interpd.register( _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} The location, relative to the parent Axes, where the colorbar Axes @@ -86,11 +86,6 @@ If *False* the minimum and maximum colorbar extensions will be triangular (the default). If *True* the extensions will be rectangular. -spacing : {'uniform', 'proportional'} - For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each - color the same space; 'proportional' makes the space proportional to the - data interval. - ticks : None or list of ticks or Locator If None, ticks are determined automatically from the input. @@ -109,9 +104,15 @@ If unset, the colormap will be displayed on a 0-1 scale. If sequences, *values* must have a length 1 less than *boundaries*. For each region delimited by adjacent entries in *boundaries*, the color mapped - to the corresponding value in values will be used. + to the corresponding value in *values* will be used. The size of each + region is determined by the *spacing* parameter. Normally only useful for indexed colors (i.e. ``norm=NoNorm()``) or other - unusual circumstances.""") + unusual circumstances. + +spacing : {'uniform', 'proportional'} + For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each + color the same space; 'proportional' makes the space proportional to the + data interval.""") def _set_ticks_on_axis_warn(*args, **kwargs): @@ -429,44 +430,51 @@ def __init__(self, ax, mappable=None, *, cmap=None, self._extend_cid2 = self.ax.callbacks.connect( "ylim_changed", self._do_extends) + @property + def long_axis(self): + """Axis that has decorations (ticks, etc) on it.""" + if self.orientation == 'vertical': + return self.ax.yaxis + return self.ax.xaxis + @property def locator(self): """Major tick `.Locator` for the colorbar.""" - return self._long_axis().get_major_locator() + return self.long_axis.get_major_locator() @locator.setter def locator(self, loc): - self._long_axis().set_major_locator(loc) + self.long_axis.set_major_locator(loc) self._locator = loc @property def minorlocator(self): """Minor tick `.Locator` for the colorbar.""" - return self._long_axis().get_minor_locator() + return self.long_axis.get_minor_locator() @minorlocator.setter def minorlocator(self, loc): - self._long_axis().set_minor_locator(loc) + self.long_axis.set_minor_locator(loc) self._minorlocator = loc @property def formatter(self): """Major tick label `.Formatter` for the colorbar.""" - return self._long_axis().get_major_formatter() + return self.long_axis.get_major_formatter() @formatter.setter def formatter(self, fmt): - self._long_axis().set_major_formatter(fmt) + self.long_axis.set_major_formatter(fmt) self._formatter = fmt @property def minorformatter(self): """Minor tick `.Formatter` for the colorbar.""" - return self._long_axis().get_minor_formatter() + return self.long_axis.get_minor_formatter() @minorformatter.setter def minorformatter(self, fmt): - self._long_axis().set_minor_formatter(fmt) + self.long_axis.set_minor_formatter(fmt) self._minorformatter = fmt def _cbar_cla(self): @@ -477,7 +485,7 @@ def _cbar_cla(self): del self.ax.cla self.ax.cla() - def update_normal(self, mappable): + def update_normal(self, mappable=None): """ Update solid patches, lines, etc. @@ -490,12 +498,21 @@ def update_normal(self, mappable): changes values of *vmin*, *vmax* or *cmap* then the old formatter and locator will be preserved. """ - _log.debug('colorbar update normal %r %r', mappable.norm, self.norm) - self.mappable = mappable - self.set_alpha(mappable.get_alpha()) - self.cmap = mappable.cmap - if mappable.norm != self.norm: - self.norm = mappable.norm + if mappable: + # The mappable keyword argument exists because + # ScalarMappable.changed() emits self.callbacks.process('changed', self) + # in contrast, ColorizingArtist (and Colorizer) does not use this keyword. + # [ColorizingArtist.changed() emits self.callbacks.process('changed')] + # Also, there is no test where self.mappable == mappable is not True + # and possibly no use case. + # Therefore, the mappable keyword can be deprecated if cm.ScalarMappable + # is removed. + self.mappable = mappable + _log.debug('colorbar update normal %r %r', self.mappable.norm, self.norm) + self.set_alpha(self.mappable.get_alpha()) + self.cmap = self.mappable.cmap + if self.mappable.norm != self.norm: + self.norm = self.mappable.norm self._reset_locator_formatter_scale() self._draw_all() @@ -516,7 +533,7 @@ def _draw_all(self): else: if mpl.rcParams['xtick.minor.visible']: self.minorticks_on() - self._long_axis().set(label_position=self.ticklocation, + self.long_axis.set(label_position=self.ticklocation, ticks_position=self.ticklocation) self._short_axis().set_ticks([]) self._short_axis().set_ticks([], minor=True) @@ -535,7 +552,7 @@ def _draw_all(self): # also adds the outline path to self.outline spine: self._do_extends() lower, upper = self.vmin, self.vmax - if self._long_axis().get_inverted(): + if self.long_axis.get_inverted(): # If the axis is inverted, we need to swap the vmin/vmax lower, upper = upper, lower if self.orientation == 'vertical': @@ -676,7 +693,7 @@ def _do_extends(self, ax=None): if self.orientation == 'horizontal': xy = xy[:, ::-1] # add the patch - val = -1 if self._long_axis().get_inverted() else 0 + val = -1 if self.long_axis.get_inverted() else 0 color = self.cmap(self.norm(self._values[val])) patch = mpatches.PathPatch( mpath.Path(xy), facecolor=color, alpha=self.alpha, @@ -700,7 +717,7 @@ def _do_extends(self, ax=None): if self.orientation == 'horizontal': xy = xy[:, ::-1] # add the patch - val = 0 if self._long_axis().get_inverted() else -1 + val = 0 if self.long_axis.get_inverted() else -1 color = self.cmap(self.norm(self._values[val])) hatch_idx = len(self._y) - 1 patch = mpatches.PathPatch( @@ -802,9 +819,9 @@ def update_ticks(self): """ # Get the locator and formatter; defaults to self._locator if not None. self._get_ticker_locator_formatter() - self._long_axis().set_major_locator(self._locator) - self._long_axis().set_minor_locator(self._minorlocator) - self._long_axis().set_major_formatter(self._formatter) + self.long_axis.set_major_locator(self._locator) + self.long_axis.set_minor_locator(self._minorlocator) + self.long_axis.set_major_formatter(self._formatter) def _get_ticker_locator_formatter(self): """ @@ -839,15 +856,15 @@ def _get_ticker_locator_formatter(self): if locator is None: # we haven't set the locator explicitly, so use the default # for this axis: - locator = self._long_axis().get_major_locator() + locator = self.long_axis.get_major_locator() if minorlocator is None: - minorlocator = self._long_axis().get_minor_locator() + minorlocator = self.long_axis.get_minor_locator() if minorlocator is None: minorlocator = ticker.NullLocator() if formatter is None: - formatter = self._long_axis().get_major_formatter() + formatter = self.long_axis.get_major_formatter() self._locator = locator self._formatter = formatter @@ -871,12 +888,12 @@ def set_ticks(self, ticks, *, labels=None, minor=False, **kwargs): pass *labels*. In other cases, please use `~.Axes.tick_params`. """ if np.iterable(ticks): - self._long_axis().set_ticks(ticks, labels=labels, minor=minor, + self.long_axis.set_ticks(ticks, labels=labels, minor=minor, **kwargs) - self._locator = self._long_axis().get_major_locator() + self._locator = self.long_axis.get_major_locator() else: self._locator = ticks - self._long_axis().set_major_locator(self._locator) + self.long_axis.set_major_locator(self._locator) self.stale = True def get_ticks(self, minor=False): @@ -889,9 +906,9 @@ def get_ticks(self, minor=False): if True return the minor ticks. """ if minor: - return self._long_axis().get_minorticklocs() + return self.long_axis.get_minorticklocs() else: - return self._long_axis().get_majorticklocs() + return self.long_axis.get_majorticklocs() def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): """ @@ -926,7 +943,7 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): **kwargs `.Text` properties for the labels. """ - self._long_axis().set_ticklabels(ticklabels, minor=minor, **kwargs) + self.long_axis.set_ticklabels(ticklabels, minor=minor, **kwargs) def minorticks_on(self): """ @@ -938,7 +955,7 @@ def minorticks_on(self): def minorticks_off(self): """Turn the minor ticks of the colorbar off.""" self._minorlocator = ticker.NullLocator() - self._long_axis().set_minor_locator(self._minorlocator) + self.long_axis.set_minor_locator(self._minorlocator) def set_label(self, label, *, loc=None, **kwargs): """ @@ -1003,7 +1020,7 @@ def _set_scale(self, scale, **kwargs): `matplotlib.scale.register_scale`. These scales can then also be used here. """ - self._long_axis()._set_axes_scale(scale, **kwargs) + self.long_axis._set_axes_scale(scale, **kwargs) def remove(self): """ @@ -1115,10 +1132,8 @@ def _mesh(self): # Update the norm values in a context manager as it is only # a temporary change and we don't want to propagate any signals # attached to the norm (callbacks.blocked). - with self.norm.callbacks.blocked(), \ - cbook._setattr_cm(self.norm, - vmin=self.vmin, - vmax=self.vmax): + with (self.norm.callbacks.blocked(), + cbook._setattr_cm(self.norm, vmin=self.vmin, vmax=self.vmax)): y = self.norm.inverse(y) self._y = y X, Y = np.meshgrid([0., 1.], y) @@ -1277,20 +1292,14 @@ def _get_extension_lengths(self, frac, automin, automax, default=0.05): def _extend_lower(self): """Return whether the lower limit is open ended.""" - minmax = "max" if self._long_axis().get_inverted() else "min" + minmax = "max" if self.long_axis.get_inverted() else "min" return self.extend in ('both', minmax) def _extend_upper(self): """Return whether the upper limit is open ended.""" - minmax = "min" if self._long_axis().get_inverted() else "max" + minmax = "min" if self.long_axis.get_inverted() else "max" return self.extend in ('both', minmax) - def _long_axis(self): - """Return the long axis""" - if self.orientation == 'vertical': - return self.ax.yaxis - return self.ax.xaxis - def _short_axis(self): """Return the short axis""" if self.orientation == 'vertical': diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index f71c5759fc55..07467ca74f3d 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -1,6 +1,7 @@ import matplotlib.spines as mspines -from matplotlib import cm, collections, colors, contour +from matplotlib import cm, collections, colors, contour, colorizer from matplotlib.axes import Axes +from matplotlib.axis import Axis from matplotlib.backend_bases import RendererBase from matplotlib.patches import Patch from matplotlib.ticker import Locator, Formatter @@ -21,7 +22,7 @@ class _ColorbarSpine(mspines.Spines): class Colorbar: n_rasterize: int - mappable: cm.ScalarMappable + mappable: cm.ScalarMappable | colorizer.ColorizingArtist ax: Axes alpha: float | None cmap: colors.Colormap @@ -43,7 +44,7 @@ class Colorbar: def __init__( self, ax: Axes, - mappable: cm.ScalarMappable | None = ..., + mappable: cm.ScalarMappable | colorizer.ColorizingArtist | None = ..., *, cmap: str | colors.Colormap | None = ..., norm: colors.Normalize | None = ..., @@ -63,6 +64,8 @@ class Colorbar: location: Literal["left", "right", "top", "bottom"] | None = ... ) -> None: ... @property + def long_axis(self) -> Axis: ... + @property def locator(self) -> Locator: ... @locator.setter def locator(self, loc: Locator) -> None: ... @@ -78,7 +81,7 @@ class Colorbar: def minorformatter(self) -> Formatter: ... @minorformatter.setter def minorformatter(self, fmt: Formatter) -> None: ... - def update_normal(self, mappable: cm.ScalarMappable) -> None: ... + def update_normal(self, mappable: cm.ScalarMappable | None = ...) -> None: ... @overload def add_lines(self, CS: contour.ContourSet, erase: bool = ...) -> None: ... @overload diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py new file mode 100644 index 000000000000..4aebe7d0f5dc --- /dev/null +++ b/lib/matplotlib/colorizer.py @@ -0,0 +1,692 @@ +""" +The Colorizer class which handles the data to color pipeline via a +normalization and a colormap. + +.. admonition:: Provisional status of colorizer + + The ``colorizer`` module and classes in this file are considered + provisional and may change at any time without a deprecation period. + +.. seealso:: + + :doc:`/gallery/color/colormap_reference` for a list of builtin colormaps. + + :ref:`colormap-manipulation` for examples of how to make colormaps. + + :ref:`colormaps` for an in-depth discussion of choosing colormaps. + + :ref:`colormapnorms` for more details about data normalization. + +""" + +import functools + +import numpy as np +from numpy import ma + +from matplotlib import _api, colors, cbook, scale, artist +import matplotlib as mpl + +mpl._docstring.interpd.register( + colorizer_doc="""\ +colorizer : `~matplotlib.colorizer.Colorizer` or None, default: None + The Colorizer object used to map color to data. If None, a Colorizer + object is created from a *norm* and *cmap*.""", + ) + + +class Colorizer: + """ + Data to color pipeline. + + This pipeline is accessible via `.Colorizer.to_rgba` and executed via + the `.Colorizer.norm` and `.Colorizer.cmap` attributes. + + Parameters + ---------- + cmap: colorbar.Colorbar or str or None, default: None + The colormap used to color data. + + norm: colors.Normalize or str or None, default: None + The normalization used to normalize the data + """ + def __init__(self, cmap=None, norm=None): + + self._cmap = None + self._set_cmap(cmap) + + self._id_norm = None + self._norm = None + self.norm = norm + + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) + self.colorbar = None + + def _scale_norm(self, norm, vmin, vmax, A): + """ + Helper for initial scaling. + + Used by public functions that create a ScalarMappable and support + parameters *vmin*, *vmax* and *norm*. This makes sure that a *norm* + will take precedence over *vmin*, *vmax*. + + Note that this method does not set the norm. + """ + if vmin is not None or vmax is not None: + self.set_clim(vmin, vmax) + if isinstance(norm, colors.Normalize): + raise ValueError( + "Passing a Normalize instance simultaneously with " + "vmin/vmax is not supported. Please pass vmin/vmax " + "directly to the norm when creating it.") + + # always resolve the autoscaling so we have concrete limits + # rather than deferring to draw time. + self.autoscale_None(A) + + @property + def norm(self): + return self._norm + + @norm.setter + def norm(self, norm): + _api.check_isinstance((colors.Normalize, 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)() + + if norm is self.norm: + # We aren't updating anything + return + + in_init = self.norm is None + # Remove the current callback and connect to the new one + if not in_init: + self.norm.callbacks.disconnect(self._id_norm) + self._norm = norm + self._id_norm = self.norm.callbacks.connect('changed', + self.changed) + if not in_init: + self.changed() + + def to_rgba(self, x, alpha=None, bytes=False, norm=True): + """ + Return a normalized RGBA array corresponding to *x*. + + In the normal case, *x* is a 1D or 2D sequence of scalars, and + the corresponding `~numpy.ndarray` of RGBA values will be returned, + based on the norm and colormap set for this Colorizer. + + There is one special case, for handling images that are already + RGB or RGBA, such as might have been read from an image file. + If *x* is an `~numpy.ndarray` with 3 dimensions, + and the last dimension is either 3 or 4, then it will be + treated as an RGB or RGBA array, and no mapping will be done. + The array can be `~numpy.uint8`, or it can be floats with + values in the 0-1 range; otherwise a ValueError will be raised. + Any NaNs or masked elements will be set to 0 alpha. + If the last dimension is 3, the *alpha* kwarg (defaulting to 1) + will be used to fill in the transparency. If the last dimension + is 4, the *alpha* kwarg is ignored; it does not + replace the preexisting alpha. A ValueError will be raised + if the third dimension is other than 3 or 4. + + In either case, if *bytes* is *False* (default), the RGBA + array will be floats in the 0-1 range; if it is *True*, + the returned RGBA array will be `~numpy.uint8` in the 0 to 255 range. + + If norm is False, no normalization of the input data is + performed, and it is assumed to be in the range (0-1). + + """ + # First check for special case, image input: + if isinstance(x, np.ndarray) and x.ndim == 3: + return self._pass_image_data(x, alpha, bytes, norm) + + # Otherwise run norm -> colormap pipeline + x = ma.asarray(x) + if norm: + x = self.norm(x) + rgba = self.cmap(x, alpha=alpha, bytes=bytes) + return rgba + + @staticmethod + def _pass_image_data(x, alpha=None, bytes=False, norm=True): + """ + Helper function to pass ndarray of shape (...,3) or (..., 4) + through `to_rgba()`, see `to_rgba()` for docstring. + """ + if x.shape[2] == 3: + if alpha is None: + alpha = 1 + if x.dtype == np.uint8: + alpha = np.uint8(alpha * 255) + m, n = x.shape[:2] + xx = np.empty(shape=(m, n, 4), dtype=x.dtype) + xx[:, :, :3] = x + xx[:, :, 3] = alpha + elif x.shape[2] == 4: + xx = x + else: + raise ValueError("Third dimension must be 3 or 4") + if xx.dtype.kind == 'f': + # If any of R, G, B, or A is nan, set to 0 + if np.any(nans := np.isnan(x)): + if x.shape[2] == 4: + xx = xx.copy() + xx[np.any(nans, axis=2), :] = 0 + + if norm and (xx.max() > 1 or xx.min() < 0): + raise ValueError("Floating point image RGB values " + "must be in the 0..1 range.") + if bytes: + xx = (xx * 255).astype(np.uint8) + elif xx.dtype == np.uint8: + if not bytes: + xx = xx.astype(np.float32) / 255 + else: + raise ValueError("Image RGB array must be uint8 or " + "floating point; found %s" % xx.dtype) + # Account for any masked entries in the original array + # If any of R, G, B, or A are masked for an entry, we set alpha to 0 + if np.ma.is_masked(x): + xx[np.any(np.ma.getmaskarray(x), axis=2), 3] = 0 + return xx + + def autoscale(self, A): + """ + Autoscale the scalar limits on the norm instance using the + current array + """ + if A is None: + raise TypeError('You must first set_array for mappable') + # If the norm's limits are updated self.changed() will be called + # through the callbacks attached to the norm + self.norm.autoscale(A) + + def autoscale_None(self, A): + """ + Autoscale the scalar limits on the norm instance using the + current array, changing only limits that are None + """ + if A is None: + raise TypeError('You must first set_array for mappable') + # If the norm's limits are updated self.changed() will be called + # through the callbacks attached to the norm + self.norm.autoscale_None(A) + + def _set_cmap(self, cmap): + """ + Set the colormap for luminance data. + + Parameters + ---------- + 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) + if not in_init: + self.changed() # Things are not set up properly yet. + + @property + def cmap(self): + return self._cmap + + @cmap.setter + def cmap(self, cmap): + self._set_cmap(cmap) + + def set_clim(self, vmin=None, vmax=None): + """ + Set the norm limits for image scaling. + + Parameters + ---------- + vmin, vmax : float + The limits. + + The limits may also be passed as a tuple (*vmin*, *vmax*) as a + 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 + if vmax is None: + try: + vmin, vmax = vmin + except (TypeError, ValueError): + pass + if vmin is not None: + self.norm.vmin = colors._sanitize_extrema(vmin) + if vmax is not None: + self.norm.vmax = colors._sanitize_extrema(vmax) + + def get_clim(self): + """ + Return the values (min, max) that are mapped to the colormap limits. + """ + return self.norm.vmin, self.norm.vmax + + def changed(self): + """ + Call this whenever the mappable is changed to notify all the + callbackSM listeners to the 'changed' signal. + """ + self.callbacks.process('changed') + self.stale = True + + @property + def vmin(self): + return self.get_clim()[0] + + @vmin.setter + def vmin(self, vmin): + self.set_clim(vmin=vmin) + + @property + def vmax(self): + return self.get_clim()[1] + + @vmax.setter + def vmax(self, vmax): + self.set_clim(vmax=vmax) + + @property + def clip(self): + return self.norm.clip + + @clip.setter + def clip(self, clip): + self.norm.clip = clip + + +class _ColorizerInterface: + """ + Base class that contains the interface to `Colorizer` objects from + a `ColorizingArtist` or `.cm.ScalarMappable`. + + Note: This class only contain functions that interface the .colorizer + attribute. Other functions that as shared between `.ColorizingArtist` + and `.cm.ScalarMappable` are not included. + """ + def _scale_norm(self, norm, vmin, vmax): + self._colorizer._scale_norm(norm, vmin, vmax, self._A) + + def to_rgba(self, x, alpha=None, bytes=False, norm=True): + """ + Return a normalized RGBA array corresponding to *x*. + + In the normal case, *x* is a 1D or 2D sequence of scalars, and + the corresponding `~numpy.ndarray` of RGBA values will be returned, + based on the norm and colormap set for this Colorizer. + + There is one special case, for handling images that are already + RGB or RGBA, such as might have been read from an image file. + If *x* is an `~numpy.ndarray` with 3 dimensions, + and the last dimension is either 3 or 4, then it will be + treated as an RGB or RGBA array, and no mapping will be done. + The array can be `~numpy.uint8`, or it can be floats with + values in the 0-1 range; otherwise a ValueError will be raised. + Any NaNs or masked elements will be set to 0 alpha. + If the last dimension is 3, the *alpha* kwarg (defaulting to 1) + will be used to fill in the transparency. If the last dimension + is 4, the *alpha* kwarg is ignored; it does not + replace the preexisting alpha. A ValueError will be raised + if the third dimension is other than 3 or 4. + + In either case, if *bytes* is *False* (default), the RGBA + array will be floats in the 0-1 range; if it is *True*, + the returned RGBA array will be `~numpy.uint8` in the 0 to 255 range. + + If norm is False, no normalization of the input data is + performed, and it is assumed to be in the range (0-1). + + """ + return self._colorizer.to_rgba(x, alpha=alpha, bytes=bytes, norm=norm) + + def get_clim(self): + """ + Return the values (min, max) that are mapped to the colormap limits. + """ + return self._colorizer.get_clim() + + def set_clim(self, vmin=None, vmax=None): + """ + Set the norm limits for image scaling. + + Parameters + ---------- + vmin, vmax : float + The limits. + + For scalar data, the limits may also be passed as a + tuple (*vmin*, *vmax*) as a 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 + self._colorizer.set_clim(vmin, vmax) + + def get_alpha(self): + try: + return super().get_alpha() + except AttributeError: + return 1 + + @property + def cmap(self): + return self._colorizer.cmap + + @cmap.setter + def cmap(self, cmap): + self._colorizer.cmap = cmap + + def get_cmap(self): + """Return the `.Colormap` instance.""" + return self._colorizer.cmap + + def set_cmap(self, cmap): + """ + Set the colormap for luminance data. + + Parameters + ---------- + cmap : `.Colormap` or str or None + """ + self.cmap = cmap + + @property + def norm(self): + return self._colorizer.norm + + @norm.setter + def norm(self, norm): + self._colorizer.norm = norm + + def set_norm(self, norm): + """ + Set the normalization instance. + + Parameters + ---------- + norm : `.Normalize` or str or None + + Notes + ----- + If there are any colorbars using the mappable for this norm, setting + the norm of the mappable will reset the norm, locator, and formatters + on the colorbar to default. + """ + self.norm = norm + + def autoscale(self): + """ + Autoscale the scalar limits on the norm instance using the + current array + """ + self._colorizer.autoscale(self._A) + + def autoscale_None(self): + """ + Autoscale the scalar limits on the norm instance using the + current array, changing only limits that are None + """ + self._colorizer.autoscale_None(self._A) + + @property + def colorbar(self): + """ + The last colorbar associated with this object. May be None + """ + return self._colorizer.colorbar + + @colorbar.setter + def colorbar(self, colorbar): + self._colorizer.colorbar = colorbar + + def _format_cursor_data_override(self, data): + # This function overwrites Artist.format_cursor_data(). We cannot + # implement cm.ScalarMappable.format_cursor_data() directly, because + # most cm.ScalarMappable subclasses inherit from Artist first and from + # cm.ScalarMappable second, so Artist.format_cursor_data would always + # have precedence over cm.ScalarMappable.format_cursor_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): + return "[]" + normed = self.norm(data) + if np.isfinite(normed): + if isinstance(self.norm, colors.BoundaryNorm): + # not an invertible normalization mapping + cur_idx = np.argmin(np.abs(self.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: + # singular norms, use delta of 10% of only value + delta = np.abs(self.norm.vmin * .1) + else: + # Midpoints of neighboring color intervals. + neighbors = self.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}]" + + +class _ScalarMappable(_ColorizerInterface): + """ + A mixin class to map one or multiple sets of scalar data to RGBA. + + The ScalarMappable applies data normalization before returning RGBA colors from + the given `~matplotlib.colors.Colormap`. + """ + + # _ScalarMappable exists for compatibility with + # code written before the introduction of the Colorizer + # and ColorizingArtist classes. + + # _ScalarMappable can be depreciated so that ColorizingArtist + # inherits directly from _ColorizerInterface. + # in this case, the following changes should occur: + # __init__() has its functionality moved to ColorizingArtist. + # set_array(), get_array(), _get_colorizer() and + # _check_exclusionary_keywords() are moved to ColorizingArtist. + # changed() can be removed so long as colorbar.Colorbar + # is changed to connect to the colorizer instead of the + # ScalarMappable/ColorizingArtist, + # otherwise changed() can be moved to ColorizingArtist. + def __init__(self, norm=None, cmap=None, *, colorizer=None, **kwargs): + """ + Parameters + ---------- + norm : `.Normalize` (or subclass thereof) or str or None + The normalizing object which scales data, typically into the + interval ``[0, 1]``. + If a `str`, a `.Normalize` subclass is dynamically generated based + on the scale with the corresponding name. + If *None*, *norm* defaults to a *colors.Normalize* object which + initializes its scaling based on the first data processed. + cmap : str or `~matplotlib.colors.Colormap` + The colormap used to map normalized data values to RGBA colors. + """ + super().__init__(**kwargs) + self._A = None + self._colorizer = self._get_colorizer(colorizer=colorizer, norm=norm, cmap=cmap) + + self.colorbar = None + self._id_colorizer = self._colorizer.callbacks.connect('changed', self.changed) + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) + + def set_array(self, A): + """ + Set the value array from array-like *A*. + + Parameters + ---------- + A : array-like or None + The values that are mapped to colors. + + The base class `.ScalarMappable` does not make any assumptions on + the dimensionality and shape of the value array *A*. + """ + if A is None: + self._A = None + return + + 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") + + self._A = A + if not self.norm.scaled(): + self._colorizer.autoscale_None(A) + + def get_array(self): + """ + Return the array of values, that are mapped to colors. + + The base class `.ScalarMappable` does not make any assumptions on + the dimensionality and shape of the array. + """ + return self._A + + def changed(self): + """ + Call this whenever the mappable is changed to notify all the + callbackSM listeners to the 'changed' signal. + """ + self.callbacks.process('changed', self) + self.stale = True + + @staticmethod + def _check_exclusionary_keywords(colorizer, **kwargs): + """ + Raises a ValueError if any kwarg is not None while colorizer is not None + """ + if colorizer is not None: + if any([val is not None for val in kwargs.values()]): + raise ValueError("The `colorizer` keyword cannot be used simultaneously" + " with any of the following keywords: " + + ", ".join(f'`{key}`' for key in kwargs.keys())) + + @staticmethod + def _get_colorizer(cmap, norm, colorizer): + if isinstance(colorizer, Colorizer): + _ScalarMappable._check_exclusionary_keywords( + Colorizer, cmap=cmap, norm=norm + ) + return colorizer + return Colorizer(cmap, norm) + +# The docstrings here must be generic enough to apply to all relevant methods. +mpl._docstring.interpd.register( + cmap_doc="""\ +cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The Colormap instance or registered colormap name used to map scalar data + to colors.""", + norm_doc="""\ +norm : str or `~matplotlib.colors.Normalize`, optional + The normalization method used to scale scalar 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. + + If given, 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 that case, a suitable `.Normalize` subclass is dynamically generated + and instantiated.""", + vmin_vmax_doc="""\ +vmin, vmax : float, 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).""", +) + + +class ColorizingArtist(_ScalarMappable, artist.Artist): + """ + Base class for artists that make map data to color using a `.colorizer.Colorizer`. + + The `.colorizer.Colorizer` applies data normalization before + returning RGBA colors from a `~matplotlib.colors.Colormap`. + + """ + def __init__(self, colorizer, **kwargs): + """ + Parameters + ---------- + colorizer : `.colorizer.Colorizer` + """ + _api.check_isinstance(Colorizer, colorizer=colorizer) + super().__init__(colorizer=colorizer, **kwargs) + + @property + def colorizer(self): + return self._colorizer + + @colorizer.setter + def colorizer(self, cl): + _api.check_isinstance(Colorizer, colorizer=cl) + self._colorizer.callbacks.disconnect(self._id_colorizer) + self._colorizer = cl + self._id_colorizer = cl.callbacks.connect('changed', self.changed) + + def _set_colorizer_check_keywords(self, colorizer, **kwargs): + """ + Raises a ValueError if any kwarg is not None while colorizer is not None. + """ + self._check_exclusionary_keywords(colorizer, **kwargs) + self.colorizer = colorizer + + +def _auto_norm_from_scale(scale_cls): + """ + Automatically generate a norm class from *scale_cls*. + + This differs from `.colors.make_norm_from_scale` in the following points: + + - This function is not a class decorator, but directly returns a norm class + (as if decorating `.Normalize`). + - The scale is automatically constructed with ``nonpositive="mask"``, if it + supports such a parameter, to work around the difference in defaults + between standard scales (which use "clip") and norms (which use "mask"). + + Note that ``make_norm_from_scale`` caches the generated norm classes + (not the instances) and reuses them for later calls. For example, + ``type(_auto_norm_from_scale("log")) == LogNorm``. + """ + # Actually try to construct an instance, to verify whether + # ``nonpositive="mask"`` is supported. + try: + norm = colors.make_norm_from_scale( + functools.partial(scale_cls, nonpositive="mask"))( + colors.Normalize)() + except TypeError: + norm = colors.make_norm_from_scale(scale_cls)( + colors.Normalize)() + return type(norm) diff --git a/lib/matplotlib/colorizer.pyi b/lib/matplotlib/colorizer.pyi new file mode 100644 index 000000000000..8fcce3e5d63b --- /dev/null +++ b/lib/matplotlib/colorizer.pyi @@ -0,0 +1,102 @@ +from matplotlib import cbook, colorbar, colors, artist + +from typing import overload +import numpy as np +from numpy.typing import ArrayLike + + +class Colorizer: + colorbar: colorbar.Colorbar | None + callbacks: cbook.CallbackRegistry + def __init__( + self, + cmap: str | colors.Colormap | None = ..., + norm: str | colors.Normalize | None = ..., + ) -> None: ... + @property + def norm(self) -> colors.Normalize: ... + @norm.setter + def norm(self, norm: colors.Normalize | str | None) -> None: ... + def to_rgba( + self, + x: np.ndarray, + alpha: float | ArrayLike | None = ..., + bytes: bool = ..., + norm: bool = ..., + ) -> np.ndarray: ... + def autoscale(self, A: ArrayLike) -> None: ... + def autoscale_None(self, A: ArrayLike) -> None: ... + @property + def cmap(self) -> colors.Colormap: ... + @cmap.setter + def cmap(self, cmap: colors.Colormap | str | None) -> None: ... + def get_clim(self) -> tuple[float, float]: ... + def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... + def changed(self) -> None: ... + @property + def vmin(self) -> float | None: ... + @vmin.setter + def vmin(self, value: float | None) -> None: ... + @property + def vmax(self) -> float | None: ... + @vmax.setter + def vmax(self, value: float | None) -> None: ... + @property + def clip(self) -> bool: ... + @clip.setter + def clip(self, value: bool) -> None: ... + + +class _ColorizerInterface: + cmap: colors.Colormap + colorbar: colorbar.Colorbar | None + callbacks: cbook.CallbackRegistry + def to_rgba( + self, + x: np.ndarray, + alpha: float | ArrayLike | None = ..., + bytes: bool = ..., + norm: bool = ..., + ) -> np.ndarray: ... + def get_clim(self) -> tuple[float, float]: ... + def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... + def get_alpha(self) -> float | None: ... + def get_cmap(self) -> colors.Colormap: ... + def set_cmap(self, cmap: str | colors.Colormap) -> None: ... + @property + def norm(self) -> colors.Normalize: ... + @norm.setter + def norm(self, norm: colors.Normalize | str | None) -> None: ... + def set_norm(self, norm: colors.Normalize | str | None) -> None: ... + def autoscale(self) -> None: ... + def autoscale_None(self) -> None: ... + + +class _ScalarMappable(_ColorizerInterface): + def __init__( + self, + norm: colors.Normalize | None = ..., + cmap: str | colors.Colormap | None = ..., + *, + colorizer: Colorizer | None = ..., + **kwargs + ) -> None: ... + def set_array(self, A: ArrayLike | None) -> None: ... + def get_array(self) -> np.ndarray | None: ... + def changed(self) -> None: ... + + +class ColorizingArtist(_ScalarMappable, artist.Artist): + callbacks: cbook.CallbackRegistry + def __init__( + self, + colorizer: Colorizer, + **kwargs + ) -> None: ... + def set_array(self, A: ArrayLike | None) -> None: ... + def get_array(self) -> np.ndarray | None: ... + def changed(self) -> None: ... + @property + def colorizer(self) -> Colorizer: ... + @colorizer.setter + def colorizer(self, cl: Colorizer) -> None: ... diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2c0d1b5d9a4f..5f909d07c190 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -54,7 +54,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, _cm, cbook, scale +from matplotlib import _api, _cm, cbook, scale, _image from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -87,6 +87,7 @@ def __delitem__(self, key): _colors_full_map = _ColorMapping(_colors_full_map) _REPR_PNG_SIZE = (512, 64) +_BIVAR_REPR_PNG_SIZE = 256 def get_named_colors_mapping(): @@ -129,6 +130,7 @@ class ColorSequenceRegistry(Mapping): 'Set1': _cm._Set1_data, 'Set2': _cm._Set2_data, 'Set3': _cm._Set3_data, + 'petroff10': _cm._petroff10_data, } def __init__(self): @@ -704,6 +706,7 @@ def __init__(self, name, N=256): self._i_over = self.N + 1 self._i_bad = self.N + 2 self._isinit = False + self.n_variates = 1 #: When this colormap exists on a scalar mappable and colorbar_extend #: is not False, colorbar creation will pick up ``colorbar_extend`` as #: the default value for the ``extend`` keyword in the @@ -723,7 +726,7 @@ def __call__(self, X, alpha=None, bytes=False): alpha : float or array-like or None Alpha must be a scalar between 0 and 1, a sequence of such floats with shape matching X, or None. - bytes : bool + bytes : bool, default: False If False (default), the returned RGBA values will be floats in the interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the interval ``[0, 255]``. @@ -733,6 +736,36 @@ def __call__(self, X, alpha=None, bytes=False): Tuple of RGBA values if X is scalar, otherwise an array of RGBA values with a shape of ``X.shape + (4, )``. """ + rgba, mask = self._get_rgba_and_mask(X, alpha=alpha, bytes=bytes) + if not np.iterable(X): + rgba = tuple(rgba) + return rgba + + def _get_rgba_and_mask(self, X, alpha=None, bytes=False): + r""" + Parameters + ---------- + X : float or int, `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + For floats, *X* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap line. + For integers, *X* should be in the interval ``[0, Colormap.N)`` to + return RGBA values *indexed* from the Colormap with index ``X``. + alpha : float or array-like or None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching X, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + + Returns + ------- + colors : np.ndarray + Array of RGBA values with a shape of ``X.shape + (4, )``. + mask : np.ndarray + Boolean array with True where the input is ``np.nan`` or masked. + """ if not self._isinit: self._init() @@ -776,9 +809,7 @@ def __call__(self, X, alpha=None, bytes=False): if (lut[-1] == 0).all(): rgba[mask_bad] = (0, 0, 0, 0) - if not np.iterable(X): - rgba = tuple(rgba) - return rgba + return rgba, mask_bad def __copy__(self): cls = self.__class__ @@ -1226,6 +1257,864 @@ def reversed(self, name=None): return new_cmap +class MultivarColormap: + """ + Class for holding multiple `~matplotlib.colors.Colormap` for use in a + `~matplotlib.cm.ScalarMappable` object + """ + def __init__(self, colormaps, combination_mode, name='multivariate colormap'): + """ + Parameters + ---------- + colormaps: list or tuple of `~matplotlib.colors.Colormap` objects + The individual colormaps that are combined + combination_mode: str, 'sRGB_add' or 'sRGB_sub' + Describe how colormaps are combined in sRGB space + + - If 'sRGB_add' -> Mixing produces brighter colors + `sRGB = sum(colors)` + - If 'sRGB_sub' -> Mixing produces darker colors + `sRGB = 1 - sum(1 - colors)` + name : str, optional + The name of the colormap family. + """ + self.name = name + + if not np.iterable(colormaps) \ + or len(colormaps) == 1 \ + or isinstance(colormaps, str): + raise ValueError("A MultivarColormap must have more than one colormap.") + colormaps = list(colormaps) # ensure cmaps is a list, i.e. not a tuple + for i, cmap in enumerate(colormaps): + if isinstance(cmap, str): + colormaps[i] = mpl.colormaps[cmap] + elif not isinstance(cmap, Colormap): + raise ValueError("colormaps must be a list of objects that subclass" + " Colormap or a name found in the colormap registry.") + + self._colormaps = colormaps + _api.check_in_list(['sRGB_add', 'sRGB_sub'], combination_mode=combination_mode) + self._combination_mode = combination_mode + self.n_variates = len(colormaps) + self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. + + def __call__(self, X, alpha=None, bytes=False, clip=True): + r""" + Parameters + ---------- + X : tuple (X0, X1, ...) of length equal to the number of colormaps + X0, X1 ...: + float or int, `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + For floats, *Xi...* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap line. + For integers, *Xi...* should be in the interval ``[0, self[i].N)`` to + return RGBA values *indexed* from colormap [i] with index ``Xi``, where + self[i] is colormap i. + alpha : float or array-like or None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching *Xi*, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + clip : bool, default: True + If True, clip output to 0 to 1 + + Returns + ------- + Tuple of RGBA values if X[0] is scalar, otherwise an array of + RGBA values with a shape of ``X.shape + (4, )``. + """ + + if len(X) != len(self): + raise ValueError( + f'For the selected colormap the data must have a first dimension ' + f'{len(self)}, not {len(X)}') + rgba, mask_bad = self[0]._get_rgba_and_mask(X[0], bytes=False) + for c, xx in zip(self[1:], X[1:]): + sub_rgba, sub_mask_bad = c._get_rgba_and_mask(xx, bytes=False) + rgba[..., :3] += sub_rgba[..., :3] # add colors + rgba[..., 3] *= sub_rgba[..., 3] # multiply alpha + mask_bad |= sub_mask_bad + + if self.combination_mode == 'sRGB_sub': + rgba[..., :3] -= len(self) - 1 + + rgba[mask_bad] = self.get_bad() + + if clip: + rgba = np.clip(rgba, 0, 1) + + if alpha is not None: + if clip: + alpha = np.clip(alpha, 0, 1) + if np.shape(alpha) not in [(), np.shape(X[0])]: + raise ValueError( + f"alpha is array-like but its shape {np.shape(alpha)} does " + f"not match that of X[0] {np.shape(X[0])}") + rgba[..., -1] *= alpha + + if bytes: + if not clip: + raise ValueError( + "clip cannot be false while bytes is true" + " as uint8 does not support values below 0" + " or above 255.") + rgba = (rgba * 255).astype('uint8') + + if not np.iterable(X[0]): + rgba = tuple(rgba) + + return rgba + + def copy(self): + """Return a copy of the multivarcolormap.""" + return self.__copy__() + + def __copy__(self): + cls = self.__class__ + cmapobject = cls.__new__(cls) + cmapobject.__dict__.update(self.__dict__) + cmapobject._colormaps = [cm.copy() for cm in self._colormaps] + cmapobject._rgba_bad = np.copy(self._rgba_bad) + return cmapobject + + def __eq__(self, other): + if not isinstance(other, MultivarColormap): + return False + if len(self) != len(other): + return False + for c0, c1 in zip(self, other): + if c0 != c1: + return False + if not all(self._rgba_bad == other._rgba_bad): + return False + if self.combination_mode != other.combination_mode: + return False + return True + + def __getitem__(self, item): + return self._colormaps[item] + + def __iter__(self): + for c in self._colormaps: + yield c + + def __len__(self): + return len(self._colormaps) + + def __str__(self): + return self.name + + def get_bad(self): + """Get the color for masked values.""" + return np.array(self._rgba_bad) + + def resampled(self, lutshape): + """ + Return a new colormap with *lutshape* entries. + + Parameters + ---------- + lutshape : tuple of (`int`, `None`) + The tuple must have a length matching the number of variates. + For each element in the tuple, if `int`, the corresponding colorbar + is resampled, if `None`, the corresponding colorbar is not resampled. + + Returns + ------- + MultivarColormap + """ + + if not np.iterable(lutshape) or len(lutshape) != len(self): + raise ValueError(f"lutshape must be of length {len(self)}") + new_cmap = self.copy() + for i, s in enumerate(lutshape): + if s is not None: + new_cmap._colormaps[i] = self[i].resampled(s) + return new_cmap + + def with_extremes(self, *, bad=None, under=None, over=None): + """ + Return a copy of the `MultivarColormap` with modified out-of-range attributes. + + The *bad* keyword modifies the copied `MultivarColormap` while *under* and + *over* modifies the attributes of the copied component colormaps. + Note that *under* and *over* colors are subject to the mixing rules determined + by the *combination_mode*. + + Parameters + ---------- + bad: :mpltype:`color`, default: None + If Matplotlib color, the bad value is set accordingly in the copy + + under tuple of :mpltype:`color`, default: None + If tuple, the `under` value of each component is set with the values + from the tuple. + + over tuple of :mpltype:`color`, default: None + If tuple, the `over` value of each component is set with the values + from the tuple. + + Returns + ------- + MultivarColormap + copy of self with attributes set + + """ + new_cm = self.copy() + if bad is not None: + new_cm._rgba_bad = to_rgba(bad) + if under is not None: + if not np.iterable(under) or len(under) != len(new_cm): + raise ValueError("*under* 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, under): + c.set_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) + return new_cm + + @property + def combination_mode(self): + return self._combination_mode + + def _repr_png_(self): + """Generate a PNG representation of the Colormap.""" + X = np.tile(np.linspace(0, 1, _REPR_PNG_SIZE[0]), + (_REPR_PNG_SIZE[1], 1)) + pixels = np.zeros((_REPR_PNG_SIZE[1]*len(self), _REPR_PNG_SIZE[0], 4), + dtype=np.uint8) + for i, c in enumerate(self): + pixels[i*_REPR_PNG_SIZE[1]:(i+1)*_REPR_PNG_SIZE[1], :] = c(X, bytes=True) + png_bytes = io.BytesIO() + title = self.name + ' multivariate colormap' + author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org' + pnginfo = PngInfo() + pnginfo.add_text('Title', title) + pnginfo.add_text('Description', title) + pnginfo.add_text('Author', author) + pnginfo.add_text('Software', author) + Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo) + return png_bytes.getvalue() + + def _repr_html_(self): + """Generate an HTML representation of the MultivarColormap.""" + return ''.join([c._repr_html_() for c in self._colormaps]) + + +class BivarColormap: + """ + Base class for all bivariate to RGBA mappings. + + Designed as a drop-in replacement for Colormap when using a 2D + lookup table. To be used with `~matplotlib.cm.ScalarMappable`. + """ + + def __init__(self, N=256, M=256, shape='square', origin=(0, 0), + name='bivariate colormap'): + """ + Parameters + ---------- + N : int, default: 256 + The number of RGB quantization levels along the first axis. + M : int, default: 256 + The number of RGB quantization levels along the second axis. + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - 'square' each variate is clipped to [0,1] independently + - 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - 'circleignore' a circular mask is applied, but the data is not + clipped and instead assigned the 'outside' color + + origin : (float, float), default: (0,0) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + name : str, optional + The name of the colormap. + """ + + self.name = name + self.N = int(N) # ensure that N is always int + self.M = int(M) + _api.check_in_list(['square', 'circle', 'ignore', 'circleignore'], shape=shape) + self._shape = shape + self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. + self._rgba_outside = (1.0, 0.0, 1.0, 1.0) + self._isinit = False + self.n_variates = 2 + self._origin = (float(origin[0]), float(origin[1])) + '''#: When this colormap exists on a scalar mappable and colorbar_extend + #: is not False, colorbar creation will pick up ``colorbar_extend`` as + #: the default value for the ``extend`` keyword in the + #: `matplotlib.colorbar.Colorbar` constructor. + self.colorbar_extend = False''' + + def __call__(self, X, alpha=None, bytes=False): + r""" + Parameters + ---------- + X : tuple (X0, X1), X0 and X1: float or int `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + + - For floats, *X* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap. + - For integers, *X* should be in the interval ``[0, Colormap.N)`` to + return RGBA values *indexed* from the Colormap with index ``X``. + + alpha : float or array-like or None, default: None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching X0, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + + Returns + ------- + Tuple of RGBA values if X is scalar, otherwise an array of + RGBA values with a shape of ``X.shape + (4, )``. + """ + + if len(X) != 2: + raise ValueError( + f'For a `BivarColormap` the data must have a first dimension ' + f'2, not {len(X)}') + + if not self._isinit: + self._init() + + X0 = np.ma.array(X[0], copy=True) + X1 = np.ma.array(X[1], copy=True) + # clip to shape of colormap, circle square, etc. + self._clip((X0, X1)) + + # Native byteorder is faster. + if not X0.dtype.isnative: + X0 = X0.byteswap().view(X0.dtype.newbyteorder()) + if not X1.dtype.isnative: + X1 = X1.byteswap().view(X1.dtype.newbyteorder()) + + if X0.dtype.kind == "f": + X0 *= self.N + # xa == 1 (== N after multiplication) is not out of range. + X0[X0 == self.N] = self.N - 1 + + if X1.dtype.kind == "f": + X1 *= self.M + # xa == 1 (== N after multiplication) is not out of range. + X1[X1 == self.M] = self.M - 1 + + # Pre-compute the masks before casting to int (which can truncate) + mask_outside = (X0 < 0) | (X1 < 0) | (X0 >= self.N) | (X1 >= self.M) + # If input was masked, get the bad mask from it; else mask out nans. + mask_bad_0 = X0.mask if np.ma.is_masked(X0) else np.isnan(X0) + mask_bad_1 = X1.mask if np.ma.is_masked(X1) else np.isnan(X1) + mask_bad = mask_bad_0 | mask_bad_1 + + with np.errstate(invalid="ignore"): + # We need this cast for unsigned ints as well as floats + X0 = X0.astype(int) + X1 = X1.astype(int) + + # Set masked values to zero + # The corresponding rgb values will be replaced later + for X_part in [X0, X1]: + X_part[mask_outside] = 0 + X_part[mask_bad] = 0 + + rgba = self._lut[X0, X1] + if np.isscalar(X[0]): + rgba = np.copy(rgba) + rgba[mask_outside] = self._rgba_outside + rgba[mask_bad] = self._rgba_bad + if bytes: + rgba = (rgba * 255).astype(np.uint8) + if alpha is not None: + alpha = np.clip(alpha, 0, 1) + if bytes: + alpha *= 255 # Will be cast to uint8 upon assignment. + if np.shape(alpha) not in [(), np.shape(X0)]: + raise ValueError( + f"alpha is array-like but its shape {np.shape(alpha)} does " + f"not match that of X[0] {np.shape(X0)}") + rgba[..., -1] = alpha + # If the "bad" color is all zeros, then ignore alpha input. + if (np.array(self._rgba_bad) == 0).all(): + rgba[mask_bad] = (0, 0, 0, 0) + + if not np.iterable(X[0]): + rgba = tuple(rgba) + return rgba + + @property + def lut(self): + """ + For external access to the lut, i.e. for displaying the cmap. + For circular colormaps this returns a lut with a circular mask. + + Internal functions (such as to_rgb()) should use _lut + which stores the lut without a circular mask + A lut without the circular mask is needed in to_rgb() because the + conversion from floats to ints results in some some pixel-requests + just outside of the circular mask + + """ + if not self._isinit: + self._init() + lut = np.copy(self._lut) + if self.shape == 'circle' or self.shape == 'circleignore': + n = np.linspace(-1, 1, self.N) + m = np.linspace(-1, 1, self.M) + radii_sqr = (n**2)[:, np.newaxis] + (m**2)[np.newaxis, :] + mask_outside = radii_sqr > 1 + lut[mask_outside, 3] = 0 + return lut + + def __copy__(self): + cls = self.__class__ + cmapobject = cls.__new__(cls) + cmapobject.__dict__.update(self.__dict__) + + cmapobject._rgba_outside = np.copy(self._rgba_outside) + cmapobject._rgba_bad = np.copy(self._rgba_bad) + cmapobject._shape = self.shape + if self._isinit: + cmapobject._lut = np.copy(self._lut) + return cmapobject + + def __eq__(self, other): + if not isinstance(other, BivarColormap): + return False + # To compare lookup tables the Colormaps have to be initialized + if not self._isinit: + self._init() + if not other._isinit: + other._init() + if not np.array_equal(self._lut, other._lut): + return False + if not np.array_equal(self._rgba_bad, other._rgba_bad): + return False + if not np.array_equal(self._rgba_outside, other._rgba_outside): + return False + if self.shape != other.shape: + return False + return True + + def get_bad(self): + """Get the color for masked values.""" + return self._rgba_bad + + def get_outside(self): + """Get the color for out-of-range values.""" + return self._rgba_outside + + def resampled(self, lutshape, transposed=False): + """ + Return a new colormap with *lutshape* entries. + + Note that this function does not move the origin. + + Parameters + ---------- + lutshape : tuple of ints or None + The tuple must be of length 2, and each entry is either an int or None. + + - If an int, the corresponding axis is resampled. + - If negative the corresponding axis is resampled in reverse + - If -1, the axis is inverted + - If 1 or None, the corresponding axis is not resampled. + + transposed : bool, default: False + if True, the axes are swapped after resampling + + Returns + ------- + BivarColormap + """ + + if not np.iterable(lutshape) or len(lutshape) != 2: + raise ValueError("lutshape must be of length 2") + lutshape = [lutshape[0], lutshape[1]] + if lutshape[0] is None or lutshape[0] == 1: + lutshape[0] = self.N + if lutshape[1] is None or lutshape[1] == 1: + lutshape[1] = self.M + + inverted = [False, False] + if lutshape[0] < 0: + inverted[0] = True + lutshape[0] = -lutshape[0] + if lutshape[0] == 1: + lutshape[0] = self.N + if lutshape[1] < 0: + inverted[1] = True + lutshape[1] = -lutshape[1] + if lutshape[1] == 1: + lutshape[1] = self.M + x_0, x_1 = np.mgrid[0:1:(lutshape[0] * 1j), 0:1:(lutshape[1] * 1j)] + if inverted[0]: + x_0 = x_0[::-1, :] + if inverted[1]: + x_1 = x_1[:, ::-1] + + # we need to use shape = 'square' while resampling the colormap. + # if the colormap has shape = 'circle' we would otherwise get *outside* in the + # resampled colormap + shape_memory = self._shape + self._shape = 'square' + if transposed: + new_lut = self((x_1, x_0)) + new_cmap = BivarColormapFromImage(new_lut, name=self.name, + shape=shape_memory, + origin=self.origin[::-1]) + else: + new_lut = self((x_0, x_1)) + new_cmap = BivarColormapFromImage(new_lut, name=self.name, + shape=shape_memory, + origin=self.origin) + self._shape = shape_memory + + new_cmap._rgba_bad = self._rgba_bad + new_cmap._rgba_outside = self._rgba_outside + return new_cmap + + def reversed(self, axis_0=True, axis_1=True): + """ + Reverses both or one of the axis. + """ + r_0 = -1 if axis_0 else 1 + r_1 = -1 if axis_1 else 1 + return self.resampled((r_0, r_1)) + + def transposed(self): + """ + Transposes the colormap by swapping the order of the axis + """ + return self.resampled((None, None), transposed=True) + + def with_extremes(self, *, bad=None, outside=None, shape=None, origin=None): + """ + Return a copy of the `BivarColormap` with modified attributes. + + Note that the *outside* color is only relevant if `shape` = 'ignore' + or 'circleignore'. + + Parameters + ---------- + bad : None or :mpltype:`color` + If Matplotlib color, the *bad* value is set accordingly in the copy + + outside : None or :mpltype:`color` + If Matplotlib color and shape is 'ignore' or 'circleignore', values + *outside* the colormap are colored accordingly in the copy + + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + *outside* color + - If 'circleignore' a circular mask is applied, but the data is not + clipped and instead assigned the *outside* color + + origin : (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + + Returns + ------- + BivarColormap + copy of self with attributes set + """ + new_cm = self.copy() + if bad is not None: + new_cm._rgba_bad = to_rgba(bad) + if outside is not None: + new_cm._rgba_outside = to_rgba(outside) + if shape is not None: + _api.check_in_list(['square', 'circle', 'ignore', 'circleignore'], + shape=shape) + new_cm._shape = shape + if origin is not None: + new_cm._origin = (float(origin[0]), float(origin[1])) + + return new_cm + + def _init(self): + """Generate the lookup table, ``self._lut``.""" + raise NotImplementedError("Abstract class only") + + @property + def shape(self): + return self._shape + + @property + def origin(self): + return self._origin + + def _clip(self, X): + """ + For internal use when applying a BivarColormap to data. + i.e. cm.ScalarMappable().to_rgba() + Clips X[0] and X[1] according to 'self.shape'. + X is modified in-place. + + Parameters + ---------- + X: np.array + array of floats or ints to be clipped + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap. + It is assumed that a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + and instead assigned the 'outside' color + + """ + if self.shape == 'square': + for X_part, mx in zip(X, (self.N, self.M)): + X_part[X_part < 0] = 0 + if X_part.dtype.kind == "f": + X_part[X_part > 1] = 1 + else: + X_part[X_part >= mx] = mx - 1 + + elif self.shape == 'ignore': + for X_part, mx in zip(X, (self.N, self.M)): + X_part[X_part < 0] = -1 + if X_part.dtype.kind == "f": + X_part[X_part > 1] = -1 + else: + X_part[X_part >= mx] = -1 + + elif self.shape == 'circle' or self.shape == 'circleignore': + for X_part in X: + if X_part.dtype.kind != "f": + raise NotImplementedError( + "Circular bivariate colormaps are only" + " implemented for use with with floats") + radii_sqr = (X[0] - 0.5)**2 + (X[1] - 0.5)**2 + mask_outside = radii_sqr > 0.25 + if self.shape == 'circle': + overextend = 2 * np.sqrt(radii_sqr[mask_outside]) + X[0][mask_outside] = (X[0][mask_outside] - 0.5) / overextend + 0.5 + X[1][mask_outside] = (X[1][mask_outside] - 0.5) / overextend + 0.5 + else: + X[0][mask_outside] = -1 + X[1][mask_outside] = -1 + + def __getitem__(self, item): + """Creates and returns a colorbar along the selected axis""" + if not self._isinit: + self._init() + 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', N=self.N) + + 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', N=self.M) + 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): + """Generate a PNG representation of the BivarColormap.""" + if not self._isinit: + self._init() + pixels = self.lut + if pixels.shape[0] < _BIVAR_REPR_PNG_SIZE: + pixels = np.repeat(pixels, + repeats=_BIVAR_REPR_PNG_SIZE//pixels.shape[0], + axis=0)[:256, :] + if pixels.shape[1] < _BIVAR_REPR_PNG_SIZE: + pixels = np.repeat(pixels, + repeats=_BIVAR_REPR_PNG_SIZE//pixels.shape[1], + axis=1)[:, :256] + pixels = (pixels[::-1, :, :] * 255).astype(np.uint8) + png_bytes = io.BytesIO() + title = self.name + ' BivarColormap' + author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org' + pnginfo = PngInfo() + pnginfo.add_text('Title', title) + pnginfo.add_text('Description', title) + pnginfo.add_text('Author', author) + pnginfo.add_text('Software', author) + Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo) + return png_bytes.getvalue() + + def _repr_html_(self): + """Generate an HTML representation of the Colormap.""" + png_bytes = self._repr_png_() + png_base64 = base64.b64encode(png_bytes).decode('ascii') + def color_block(color): + hex_color = to_hex(color, keep_alpha=True) + return (f'
') + + return ('
' + f'{self.name} ' + '
' + '
' + '
' + '
' + f'{color_block(self.get_outside())} outside' + '
' + '
' + f'bad {color_block(self.get_bad())}' + '
') + + def copy(self): + """Return a copy of the colormap.""" + return self.__copy__() + + +class SegmentedBivarColormap(BivarColormap): + """ + BivarColormap object generated by supersampling a regular grid. + + Parameters + ---------- + patch : np.array + Patch is required to have a shape (k, l, 3), and will get supersampled + to a lut of shape (N, N, 4). + N : int + The number of RGB quantization levels along each axis. + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + + origin : (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + + name : str, optional + The name of the colormap. + """ + + def __init__(self, patch, N=256, shape='square', origin=(0, 0), + name='segmented bivariate colormap'): + _api.check_shape((None, None, 3), patch=patch) + self.patch = patch + super().__init__(N, N, shape, origin, name=name) + + def _init(self): + 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) + self._isinit = True + + +class BivarColormapFromImage(BivarColormap): + """ + BivarColormap object generated by supersampling a regular grid. + + Parameters + ---------- + lut : nparray of shape (N, M, 3) or (N, M, 4) + The look-up-table + shape: {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + + origin: (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + name : str, optional + The name of the colormap. + + """ + + def __init__(self, lut, shape='square', origin=(0, 0), name='from image'): + # We can allow for a PIL.Image as input in the following way, but importing + # matplotlib.image.pil_to_array() results in a circular import + # For now, this function only accepts numpy arrays. + # i.e.: + # if isinstance(Image, lut): + # lut = image.pil_to_array(lut) + lut = np.array(lut, copy=True) + if lut.ndim != 3 or lut.shape[2] not in (3, 4): + raise ValueError("The lut must be an array of shape (n, m, 3) or (n, m, 4)", + " or a PIL.image encoded as RGB or RGBA") + + if lut.dtype == np.uint8: + lut = lut.astype(np.float32)/255 + if lut.shape[2] == 3: + new_lut = np.empty((lut.shape[0], lut.shape[1], 4), dtype=lut.dtype) + new_lut[:, :, :3] = lut + new_lut[:, :, 3] = 1. + lut = new_lut + self._lut = lut + super().__init__(lut.shape[0], lut.shape[1], shape, origin, name=name) + + def _init(self): + self._isinit = True + + class Normalize: """ A class which, when called, maps values within the interval @@ -2208,8 +3097,22 @@ def rgb_to_hsv(arr): dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints. ndmin=2, # In case input was 1D. ) + out = np.zeros_like(arr) arr_max = arr.max(-1) + # Check if input is in the expected range + if np.any(arr_max > 1): + raise ValueError( + "Input array must be in the range [0, 1]. " + f"Found a maximum value of {arr_max.max()}" + ) + + if arr.min() < 0: + raise ValueError( + "Input array must be in the range [0, 1]. " + f"Found a minimum value of {arr.min()}" + ) + ipos = arr_max > 0 delta = np.ptp(arr, -1) s = np.zeros_like(delta) diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index 514801b714b8..6941e3d5d176 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -138,6 +138,101 @@ class ListedColormap(Colormap): def resampled(self, lutsize: int) -> ListedColormap: ... def reversed(self, name: str | None = ...) -> ListedColormap: ... +class MultivarColormap: + name: str + n_variates: int + def __init__(self, colormaps: list[Colormap], combination_mode: Literal['sRGB_add', 'sRGB_sub'], name: str = ...) -> None: ... + @overload + def __call__( + self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ... + ) -> np.ndarray: ... + @overload + def __call__( + self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ..., clip: bool = ... + ) -> tuple[float, float, float, float]: ... + @overload + def __call__( + self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ... + ) -> tuple[float, float, float, float] | np.ndarray: ... + def copy(self) -> MultivarColormap: ... + def __copy__(self) -> MultivarColormap: ... + def __eq__(self, other: Any) -> bool: ... + def __getitem__(self, item: int) -> Colormap: ... + def __iter__(self) -> Iterator[Colormap]: ... + def __len__(self) -> int: ... + def get_bad(self) -> np.ndarray: ... + def resampled(self, lutshape: Sequence[int | None]) -> MultivarColormap: ... + def with_extremes( + self, + *, + bad: ColorType | None = ..., + under: Sequence[ColorType] | None = ..., + over: Sequence[ColorType] | None = ... + ) -> MultivarColormap: ... + @property + def combination_mode(self) -> str: ... + def _repr_html_(self) -> str: ... + def _repr_png_(self) -> bytes: ... + +class BivarColormap: + name: str + N: int + M: int + n_variates: int + def __init__( + self, N: int = ..., M: int | None = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + @overload + def __call__( + self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ... + ) -> np.ndarray: ... + @overload + def __call__( + self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ... + ) -> tuple[float, float, float, float]: ... + @overload + def __call__( + self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ... + ) -> tuple[float, float, float, float] | np.ndarray: ... + @property + def lut(self) -> np.ndarray: ... + @property + def shape(self) -> str: ... + @property + def origin(self) -> tuple[float, float]: ... + def copy(self) -> BivarColormap: ... + def __copy__(self) -> BivarColormap: ... + def __getitem__(self, item: int) -> Colormap: ... + def __eq__(self, other: Any) -> bool: ... + def get_bad(self) -> np.ndarray: ... + def get_outside(self) -> np.ndarray: ... + def resampled(self, lutshape: Sequence[int | None], transposed: bool = ...) -> BivarColormap: ... + def transposed(self) -> BivarColormap: ... + def reversed(self, axis_0: bool = ..., axis_1: bool = ...) -> BivarColormap: ... + def with_extremes( + self, + *, + bad: ColorType | None = ..., + outside: ColorType | None = ..., + shape: str | None = ..., + origin: None | Sequence[float] = ..., + ) -> MultivarColormap: ... + def _repr_html_(self) -> str: ... + def _repr_png_(self) -> bytes: ... + +class SegmentedBivarColormap(BivarColormap): + def __init__( + self, patch: np.ndarray, N: int = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + +class BivarColormapFromImage(BivarColormap): + def __init__( + self, lut: np.ndarray, shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + class Normalize: callbacks: cbook.CallbackRegistry def __init__( diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 3b2bf3843f4d..6b685fa0ed6a 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -27,7 +27,7 @@ def _contour_labeler_event_handler(cs, inline, inline_spacing, event): - canvas = cs.axes.figure.canvas + canvas = cs.axes.get_figure(root=True).canvas is_button = event.name == "button_press_event" is_key = event.name == "key_press_event" # Quit (even if not in infinite mode; this is consistent with @@ -199,7 +199,8 @@ def clabel(self, levels=None, *, if not inline: print('Remove last label by clicking third mouse button.') mpl._blocking_input.blocking_input_loop( - self.axes.figure, ["button_press_event", "key_press_event"], + self.axes.get_figure(root=True), + ["button_press_event", "key_press_event"], timeout=-1, handler=functools.partial( _contour_labeler_event_handler, self, inline, inline_spacing)) @@ -222,8 +223,8 @@ def too_close(self, x, y, lw): def _get_nth_label_width(self, nth): """Return the width of the *nth* label, in pixels.""" - fig = self.axes.figure - renderer = fig._get_renderer() + fig = self.axes.get_figure(root=False) + renderer = fig.get_figure(root=True)._get_renderer() return (Text(0, 0, self.get_text(self.labelLevelList[nth], self.labelFmt), figure=fig, fontproperties=self._label_font_props) @@ -310,14 +311,6 @@ def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing= determine rotation and then to break contour if desired. The extra spacing is taken into account when breaking the path, but not when computing the angle. """ - if hasattr(self, "_old_style_split_collections"): - vis = False - for coll in self._old_style_split_collections: - vis |= coll.get_visible() - coll.remove() - self.set_visible(vis) - del self._old_style_split_collections # Invalidate them. - xys = path.vertices codes = path.codes @@ -406,97 +399,6 @@ def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T] return angle, Path(xys, codes) - @_api.deprecated("3.8") - def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5): - """ - Calculate the appropriate label rotation given the linecontour - coordinates in screen units, the index of the label location and the - label width. - - If *lc* is not None or empty, also break contours and compute - inlining. - - *spacing* is the empty space to leave around the label, in pixels. - - Both tasks are done together to avoid calculating path lengths - multiple times, which is relatively costly. - - The method used here involves computing the path length along the - contour in pixel coordinates and then looking approximately (label - width / 2) away from central point to determine rotation and then to - break contour if desired. - """ - - if lc is None: - lc = [] - # Half the label width - hlw = lw / 2.0 - - # Check if closed and, if so, rotate contour so label is at edge - closed = _is_closed_polygon(slc) - if closed: - slc = np.concatenate([slc[ind:-1], slc[:ind + 1]]) - if len(lc): # Rotate lc also if not empty - lc = np.concatenate([lc[ind:-1], lc[:ind + 1]]) - ind = 0 - - # Calculate path lengths - pl = np.zeros(slc.shape[0], dtype=float) - dx = np.diff(slc, axis=0) - pl[1:] = np.cumsum(np.hypot(dx[:, 0], dx[:, 1])) - pl = pl - pl[ind] - - # Use linear interpolation to get points around label - xi = np.array([-hlw, hlw]) - if closed: # Look at end also for closed contours - dp = np.array([pl[-1], 0]) - else: - dp = np.zeros_like(xi) - - # Get angle of vector between the two ends of the label - must be - # calculated in pixel space for text rotation to work correctly. - (dx,), (dy,) = (np.diff(np.interp(dp + xi, pl, slc_col)) - for slc_col in slc.T) - rotation = np.rad2deg(np.arctan2(dy, dx)) - - if self.rightside_up: - # Fix angle so text is never upside-down - rotation = (rotation + 90) % 180 - 90 - - # Break contour if desired - nlc = [] - if len(lc): - # Expand range by spacing - xi = dp + xi + np.array([-spacing, spacing]) - - # Get (integer) indices near points of interest; use -1 as marker - # for out of bounds. - I = np.interp(xi, pl, np.arange(len(pl)), left=-1, right=-1) - I = [np.floor(I[0]).astype(int), np.ceil(I[1]).astype(int)] - if I[0] != -1: - xy1 = [np.interp(xi[0], pl, lc_col) for lc_col in lc.T] - if I[1] != -1: - xy2 = [np.interp(xi[1], pl, lc_col) for lc_col in lc.T] - - # Actually break contours - if closed: - # This will remove contour if shorter than label - if all(i != -1 for i in I): - nlc.append(np.vstack([xy2, lc[I[1]:I[0]+1], xy1])) - else: - # These will remove pieces of contour if they have length zero - if I[0] != -1: - nlc.append(np.vstack([lc[:I[0]+1], xy1])) - if I[1] != -1: - nlc.append(np.vstack([xy2, lc[I[1]:]])) - - # The current implementation removes contours completely - # covered by labels. Uncomment line below to keep - # original contour if this is the preferred behavior. - # if not len(nlc): nlc = [lc] - - return rotation, nlc - def add_label(self, x, y, rotation, lev, cvalue): """Add a contour label, respecting whether *use_clabeltext* was set.""" data_x, data_y = self.axes.transData.inverted().transform((x, y)) @@ -519,12 +421,6 @@ def add_label(self, x, y, rotation, lev, cvalue): # Add label to plot here - useful for manual mode label selection self.axes.add_artist(t) - @_api.deprecated("3.8", alternative="add_label") - def add_label_clabeltext(self, x, y, rotation, lev, cvalue): - """Add contour label with `.Text.set_transform_rotates_text`.""" - with cbook._setattr_cm(self, _use_clabeltext=True): - self.add_label(x, y, rotation, lev, cvalue) - def add_label_near(self, x, y, inline=True, inline_spacing=5, transform=None): """ @@ -604,15 +500,6 @@ def remove(self): text.remove() -def _is_closed_polygon(X): - """ - Return whether first and last object in a sequence are the same. These are - presumably coordinates on a polygonal curve, in which case this function - tests if that curve is closed. - """ - return np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13) - - def _find_closest_point_on_path(xys, p): """ Parameters @@ -649,7 +536,7 @@ def _find_closest_point_on_path(xys, p): return (d2s[imin], projs[imin], (imin, imin+1)) -_docstring.interpd.update(contour_set_attributes=r""" +_docstring.interpd.register(contour_set_attributes=r""" Attributes ---------- ax : `~matplotlib.axes.Axes` @@ -668,7 +555,7 @@ def _find_closest_point_on_path(xys, p): """) -@_docstring.dedent_interpd +@_docstring.interpd class ContourSet(ContourLabeler, mcoll.Collection): """ Store a set of contour lines or filled regions. @@ -716,8 +603,8 @@ def __init__(self, ax, *args, levels=None, filled=False, linewidths=None, linestyles=None, hatches=(None,), alpha=None, origin=None, extent=None, cmap=None, colors=None, norm=None, vmin=None, vmax=None, - extend='neither', antialiased=None, nchunk=0, locator=None, - transform=None, negative_linestyles=None, clip_path=None, + colorizer=None, extend='neither', antialiased=None, nchunk=0, + locator=None, transform=None, negative_linestyles=None, clip_path=None, **kwargs): """ Draw contour lines or filled regions, depending on @@ -773,6 +660,7 @@ def __init__(self, ax, *args, alpha=alpha, clip_path=clip_path, transform=transform, + colorizer=colorizer, ) self.axes = ax self.levels = levels @@ -785,6 +673,13 @@ def __init__(self, ax, *args, self.nchunk = nchunk self.locator = locator + + if colorizer: + self._set_colorizer_check_keywords(colorizer, cmap=cmap, + norm=norm, vmin=vmin, + vmax=vmax, colors=colors) + norm = colorizer.norm + cmap = colorizer.cmap if (isinstance(norm, mcolors.LogNorm) or isinstance(self.locator, ticker.LogLocator)): self.logscale = True @@ -816,6 +711,11 @@ def __init__(self, ax, *args, self._extend_min = self.extend in ['min', 'both'] self._extend_max = self.extend in ['max', 'both'] if self.colors is not None: + if mcolors.is_color_like(self.colors): + color_sequence = [self.colors] + else: + color_sequence = self.colors + ncolors = len(self.levels) if self.filled: ncolors -= 1 @@ -832,19 +732,19 @@ def __init__(self, ax, *args, total_levels = (ncolors + int(self._extend_min) + int(self._extend_max)) - if (len(self.colors) == total_levels and + if (len(color_sequence) == total_levels and (self._extend_min or self._extend_max)): use_set_under_over = True if self._extend_min: i0 = 1 - cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors) + cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors) if use_set_under_over: if self._extend_min: - cmap.set_under(self.colors[0]) + cmap.set_under(color_sequence[0]) if self._extend_max: - cmap.set_over(self.colors[-1]) + cmap.set_over(color_sequence[-1]) # label lists must be initialized here self.labelTexts = [] @@ -906,57 +806,9 @@ def __init__(self, ax, *args, allkinds = property(lambda self: [ [subp.codes for subp in p._iter_connected_components()] for p in self.get_paths()]) - tcolors = _api.deprecated("3.8")(property(lambda self: [ - (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)])) - tlinewidths = _api.deprecated("3.8")(property(lambda self: [ - (w,) for w in self.get_linewidths()])) alpha = property(lambda self: self.get_alpha()) linestyles = property(lambda self: self._orig_linestyles) - @_api.deprecated("3.8", alternative="set_antialiased or get_antialiased", - addendum="Note that get_antialiased returns an array.") - @property - def antialiased(self): - return all(self.get_antialiased()) - - @antialiased.setter - def antialiased(self, aa): - self.set_antialiased(aa) - - @_api.deprecated("3.8") - @property - def collections(self): - # On access, make oneself invisible and instead add the old-style collections - # (one PathCollection per level). We do not try to further split contours into - # connected components as we already lost track of what pairs of contours need - # to be considered as single units to draw filled regions with holes. - if not hasattr(self, "_old_style_split_collections"): - self.set_visible(False) - fcs = self.get_facecolor() - ecs = self.get_edgecolor() - lws = self.get_linewidth() - lss = self.get_linestyle() - self._old_style_split_collections = [] - for idx, path in enumerate(self._paths): - pc = mcoll.PathCollection( - [path] if len(path.vertices) else [], - alpha=self.get_alpha(), - antialiaseds=self._antialiaseds[idx % len(self._antialiaseds)], - transform=self.get_transform(), - zorder=self.get_zorder(), - label="_nolegend_", - facecolor=fcs[idx] if len(fcs) else "none", - edgecolor=ecs[idx] if len(ecs) else "none", - linewidths=[lws[idx % len(lws)]], - linestyles=[lss[idx % len(lss)]], - ) - if self.filled: - pc.set(hatch=self.hatches[idx % len(self.hatches)]) - self._old_style_split_collections.append(pc) - for col in self._old_style_split_collections: - self.axes.add_collection(col) - return self._old_style_split_collections - def get_transform(self): """Return the `.Transform` instance used by this ContourSet.""" if self._transform is None: @@ -1429,7 +1281,7 @@ def draw(self, renderer): super().draw(renderer) -@_docstring.dedent_interpd +@_docstring.interpd class QuadContourSet(ContourSet): """ Create and store a set of contour lines or filled regions. @@ -1610,7 +1462,7 @@ def _initialize_x_y(self, z): return np.meshgrid(x, y) -_docstring.interpd.update(contour_doc=""" +_docstring.interpd.register(contour_doc=""" `.contour` and `.contourf` draw contour lines and filled contours, respectively. Except as noted, function signatures and return values are the same for both versions. @@ -1664,10 +1516,12 @@ def _initialize_x_y(self, z): The sequence is cycled for the levels in ascending order. If the sequence is shorter than the number of levels, it's repeated. - As a shortcut, single color strings may be used in place of - one-element lists, i.e. ``'red'`` instead of ``['red']`` to color - all levels with the same color. This shortcut does only work for - color strings, not for other ways of specifying colors. + As a shortcut, a single color may be used in place of one-element lists, i.e. + ``'red'`` instead of ``['red']`` to color all levels with the same color. + + .. versionchanged:: 3.10 + Previously a single color had to be expressed as a string, but now any + valid color format may be passed. By default (value *None*), the colormap specified by *cmap* will be used. @@ -1690,6 +1544,10 @@ def _initialize_x_y(self, z): This parameter is ignored if *colors* is set. +%(colorizer_doc)s + + This parameter is ignored if *colors* is set. + origin : {*None*, 'upper', 'lower', 'image'}, default: None Determines the orientation and exact position of *Z* by specifying the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y* @@ -1735,10 +1593,10 @@ def _initialize_x_y(self, z): An existing `.QuadContourSet` does not get notified if properties of its colormap are changed. Therefore, an explicit - call `.QuadContourSet.changed()` is needed after modifying the + call `~.ContourSet.changed()` is needed after modifying the colormap. The explicit call can be left out, if a colorbar is assigned to the `.QuadContourSet` because it internally calls - `.QuadContourSet.changed()`. + `~.ContourSet.changed()`. Example:: @@ -1801,7 +1659,7 @@ def _initialize_x_y(self, z): specifies the line style for negative contours. If *negative_linestyles* is *None*, the default is taken from - :rc:`contour.negative_linestyles`. + :rc:`contour.negative_linestyle`. *negative_linestyles* can also be an iterable of the above strings specifying a set of linestyles to be used. If this iterable is shorter than diff --git a/lib/matplotlib/contour.pyi b/lib/matplotlib/contour.pyi index c386bea47ab7..7400fac50993 100644 --- a/lib/matplotlib/contour.pyi +++ b/lib/matplotlib/contour.pyi @@ -2,8 +2,8 @@ import matplotlib.cm as cm from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.collections import Collection, PathCollection +from matplotlib.colorizer import Colorizer, ColorizingArtist from matplotlib.colors import Colormap, Normalize -from matplotlib.font_manager import FontProperties from matplotlib.path import Path from matplotlib.patches import Patch from matplotlib.text import Text @@ -24,7 +24,7 @@ class ContourLabeler: rightside_up: bool labelLevelList: list[float] labelIndiceList: list[int] - labelMappable: cm.ScalarMappable + labelMappable: cm.ScalarMappable | ColorizingArtist labelCValueList: list[ColorType] labelXYs: list[tuple[float, float]] def clabel( @@ -51,20 +51,9 @@ class ContourLabeler: def locate_label( self, linecontour: ArrayLike, labelwidth: float ) -> tuple[float, float, float]: ... - def calc_label_rot_and_inline( - self, - slc: ArrayLike, - ind: int, - lw: float, - lc: ArrayLike | None = ..., - spacing: int = ..., - ) -> tuple[float, list[ArrayLike]]: ... def add_label( self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType ) -> None: ... - def add_label_clabeltext( - self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType - ) -> None: ... def add_label_near( self, x: float, @@ -96,12 +85,6 @@ class ContourSet(ContourLabeler, Collection): clip_path: Patch | Path | TransformedPath | TransformedPatchPath | None labelTexts: list[Text] labelCValues: list[ColorType] - @property - def tcolors(self) -> list[tuple[tuple[float, float, float, float]]]: ... - - # only for not filled - @property - def tlinewidths(self) -> list[tuple[float]]: ... @property def allkinds(self) -> list[list[np.ndarray | None]]: ... @@ -110,12 +93,6 @@ class ContourSet(ContourLabeler, Collection): @property def alpha(self) -> float | None: ... @property - def antialiased(self) -> bool: ... - @antialiased.setter - def antialiased(self, aa: bool | Sequence[bool]) -> None: ... - @property - def collections(self) -> list[PathCollection]: ... - @property def linestyles(self) -> ( None | Literal["solid", "dashed", "dashdot", "dotted"] | @@ -141,6 +118,7 @@ class ContourSet(ContourLabeler, Collection): norm: str | Normalize | None = ..., vmin: float | None = ..., vmax: float | None = ..., + colorizer: Colorizer | None = ..., extend: Literal["neither", "both", "min", "max"] = ..., antialiased: bool | None = ..., nchunk: int = ..., diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index c12d9f31ba4b..511e1c6df6cc 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -37,7 +37,7 @@ is achievable for (approximately) 70 years on either side of the epoch, and 20 microseconds for the rest of the allowable range of dates (year 0001 to 9999). The epoch can be changed at import time via `.dates.set_epoch` or -:rc:`dates.epoch` to other dates if necessary; see +:rc:`date.epoch` to other dates if necessary; see :doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -267,7 +267,7 @@ def set_epoch(epoch): """ Set the epoch (origin for dates) for datetime calculations. - The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00). + The default epoch is :rc:`date.epoch`. If microsecond accuracy is desired, the date being plotted needs to be within approximately 70 years of the epoch. Matplotlib internally @@ -796,7 +796,12 @@ def format_ticks(self, values): if show_offset: # set the offset string: - self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) + if (self._locator.axis and + self._locator.axis.__name__ in ('xaxis', 'yaxis') + and self._locator.axis.get_inverted()): + self.offset_string = tickdatetime[0].strftime(offsetfmts[level]) + else: + self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) if self._usetex: self.offset_string = _wrap_in_tex(self.offset_string) else: diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 82f43b56292d..bd21367ce73d 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -132,20 +132,20 @@ def glyph_name_or_index(self): # raw: Return delta as is. raw=lambda dvi, delta: delta, # u1: Read 1 byte as an unsigned number. - u1=lambda dvi, delta: dvi._arg(1, signed=False), + u1=lambda dvi, delta: dvi._read_arg(1, signed=False), # u4: Read 4 bytes as an unsigned number. - u4=lambda dvi, delta: dvi._arg(4, signed=False), + u4=lambda dvi, delta: dvi._read_arg(4, signed=False), # s4: Read 4 bytes as a signed number. - s4=lambda dvi, delta: dvi._arg(4, signed=True), + s4=lambda dvi, delta: dvi._read_arg(4, signed=True), # slen: Read delta bytes as a signed number, or None if delta is None. - slen=lambda dvi, delta: dvi._arg(delta, signed=True) if delta else None, + slen=lambda dvi, delta: dvi._read_arg(delta, signed=True) if delta else None, # slen1: Read (delta + 1) bytes as a signed number. - slen1=lambda dvi, delta: dvi._arg(delta + 1, signed=True), + slen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=True), # ulen1: Read (delta + 1) bytes as an unsigned number. - ulen1=lambda dvi, delta: dvi._arg(delta + 1, signed=False), + ulen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=False), # olen1: Read (delta + 1) bytes as an unsigned number if less than 4 bytes, # as a signed number if 4 bytes. - olen1=lambda dvi, delta: dvi._arg(delta + 1, signed=(delta == 3)), + olen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=(delta == 3)), ) @@ -230,6 +230,7 @@ def __init__(self, filename, dpi): self.dpi = dpi self.fonts = {} self.state = _dvistate.pre + self._missing_font = None def __enter__(self): """Context manager enter method, does nothing.""" @@ -270,7 +271,8 @@ def _output(self): Output the text and boxes belonging to the most recent page. page = dvi._output() """ - minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf + minx = miny = np.inf + maxx = maxy = -np.inf maxy_pure = -np.inf for elt in self.text + self.boxes: if isinstance(elt, Box): @@ -337,6 +339,8 @@ def _read(self): while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) + if self._missing_font: + raise self._missing_font.to_exception() name = self._dtable[byte].__name__ if name == "_push": down_stack.append(down_stack[-1]) @@ -354,7 +358,7 @@ def _read(self): self.close() return False - def _arg(self, nbytes, signed=False): + def _read_arg(self, nbytes, signed=False): """ Read and return a big-endian integer *nbytes* long. Signedness is determined by the *signed* keyword. @@ -364,11 +368,15 @@ def _arg(self, nbytes, signed=False): @_dispatch(min=0, max=127, state=_dvistate.inpage) def _set_char_immediate(self, char): self._put_char_real(char) + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): + return self.h += self.fonts[self.f]._width_of(char) @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',)) def _set_char(self, char): self._put_char_real(char) + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): + return self.h += self.fonts[self.f]._width_of(char) @_dispatch(132, state=_dvistate.inpage, args=('s4', 's4')) @@ -382,7 +390,9 @@ def _put_char(self, char): def _put_char_real(self, char): font = self.fonts[self.f] - if font._vf is None: + if isinstance(font, cbook._ExceptionInfo): + self._missing_font = font + elif font._vf is None: self.text.append(Text(self.h, self.v, font, char, font._width_of(char))) else: @@ -413,7 +423,7 @@ def _nop(self, _): @_dispatch(139, state=_dvistate.outer, args=('s4',)*11) def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p): self.state = _dvistate.inpage - self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 + self.h = self.v = self.w = self.x = self.y = self.z = 0 self.stack = [] self.text = [] # list of Text objects self.boxes = [] # list of Box objects @@ -486,9 +496,18 @@ 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') - tfm = _tfmfile(fontname) + try: + tfm = _tfmfile(fontname) + except FileNotFoundError as exc: + # 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 + # checks whether a missing glyph has been used, and in that case + # skips the glyph definition. + self.fonts[k] = cbook._ExceptionInfo.from_exception(exc) + return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: - raise ValueError('tfm checksum mismatch: %s' % n) + raise ValueError(f'tfm checksum mismatch: {n}') try: vf = _vffile(fontname) except FileNotFoundError: @@ -499,7 +518,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file if i != 2: - raise ValueError("Unknown dvi format %d" % i) + raise ValueError(f"Unknown dvi format {i}") if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") # meaning: TeX always uses those exact values, so it @@ -660,8 +679,8 @@ def _read(self): Read one page from the file. Return True if successful, False if there were no more pages. """ - packet_char, packet_ends = None, None - packet_len, packet_width = None, None + packet_char = packet_ends = None + packet_len = packet_width = None while True: byte = self.file.read(1)[0] # If we are in a packet, execute the dvi instructions @@ -669,62 +688,73 @@ def _read(self): byte_at = self.file.tell()-1 if byte_at == packet_ends: self._finalize_packet(packet_char, packet_width) - packet_len, packet_char, packet_width = None, None, None + packet_len = packet_char = packet_width = None # fall through to out-of-packet code elif byte_at > packet_ends: raise ValueError("Packet length mismatch in vf file") else: if byte in (139, 140) or byte >= 243: - raise ValueError( - "Inappropriate opcode %d in vf file" % byte) + raise ValueError(f"Inappropriate opcode {byte} in vf file") Dvi._dtable[byte](self, byte) continue # We are outside a packet if byte < 242: # a short packet (length given by byte) packet_len = byte - packet_char, packet_width = self._arg(1), self._arg(3) + packet_char = self._read_arg(1) + packet_width = self._read_arg(3) packet_ends = self._init_packet(byte) self.state = _dvistate.inpage elif byte == 242: # a long packet - packet_len, packet_char, packet_width = \ - [self._arg(x) for x in (4, 4, 4)] + packet_len = self._read_arg(4) + packet_char = self._read_arg(4) + packet_width = self._read_arg(4) self._init_packet(packet_len) elif 243 <= byte <= 246: - k = self._arg(byte - 242, byte == 246) - c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)] + k = self._read_arg(byte - 242, byte == 246) + c = self._read_arg(4) + s = self._read_arg(4) + d = self._read_arg(4) + a = self._read_arg(1) + l = self._read_arg(1) self._fnt_def_real(k, c, s, d, a, l) if self._first_font is None: self._first_font = k elif byte == 247: # preamble - i, k = self._arg(1), self._arg(1) + i = self._read_arg(1) + k = self._read_arg(1) x = self.file.read(k) - cs, ds = self._arg(4), self._arg(4) + cs = self._read_arg(4) + ds = self._read_arg(4) self._pre(i, x, cs, ds) elif byte == 248: # postamble (just some number of 248s) break else: - raise ValueError("Unknown vf opcode %d" % byte) + raise ValueError(f"Unknown vf opcode {byte}") def _init_packet(self, pl): if self.state != _dvistate.outer: raise ValueError("Misplaced packet in vf file") - self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 - self.stack, self.text, self.boxes = [], [], [] + self.h = self.v = self.w = self.x = self.y = self.z = 0 + self.stack = [] + self.text = [] + self.boxes = [] self.f = self._first_font + self._missing_font = None return self.file.tell() + pl def _finalize_packet(self, packet_char, packet_width): - self._chars[packet_char] = Page( - text=self.text, boxes=self.boxes, width=packet_width, - height=None, descent=None) + if not self._missing_font: # Otherwise we don't have full glyph definition. + self._chars[packet_char] = Page( + text=self.text, boxes=self.boxes, width=packet_width, + height=None, descent=None) self.state = _dvistate.outer def _pre(self, i, x, cs, ds): if self.state is not _dvistate.pre: raise ValueError("pre command in middle of vf file") if i != 202: - raise ValueError("Unknown vf format %d" % i) + raise ValueError(f"Unknown vf format {i}") if len(x): _log.debug('vf file comment: %s', x) self.state = _dvistate.outer @@ -774,7 +804,9 @@ def __init__(self, filename): widths = struct.unpack(f'!{nw}i', file.read(4*nw)) heights = struct.unpack(f'!{nh}i', file.read(4*nh)) depths = struct.unpack(f'!{nd}i', file.read(4*nd)) - self.width, self.height, self.depth = {}, {}, {} + self.width = {} + self.height = {} + self.depth = {} for idx, char in enumerate(range(bc, ec+1)): byte0 = char_info[4*idx] byte1 = char_info[4*idx+1] diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index bf5cfcbe317a..270818278f17 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -5,6 +5,7 @@ from enum import Enum from collections.abc import Generator from typing import NamedTuple +from typing_extensions import Self # < Py 3.11 class _dvistate(Enum): pre: int @@ -47,8 +48,7 @@ class Dvi: fonts: dict[int, DviFont] state: _dvistate def __init__(self, filename: str | os.PathLike, dpi: float | None) -> None: ... - # Replace return with Self when py3.9 is dropped - def __enter__(self) -> Dvi: ... + def __enter__(self) -> Self: ... def __exit__(self, etype, evalue, etrace) -> None: ... def __iter__(self) -> Generator[Page, None, None]: ... def close(self) -> None: ... @@ -83,8 +83,7 @@ class PsFont(NamedTuple): filename: str class PsfontsMap: - # Replace return with Self when py3.9 is dropped - def __new__(cls, filename: str | os.PathLike) -> PsfontsMap: ... + def __new__(cls, filename: str | os.PathLike) -> Self: ... def __getitem__(self, texname: bytes) -> PsFont: ... def find_tex_file(filename: str | os.PathLike) -> str: ... diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0d939190a0a9..3d6f9a7f4c16 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -6,9 +6,8 @@ Many methods are implemented in `FigureBase`. `SubFigure` - A logical figure inside a figure, usually added to a figure (or parent - `SubFigure`) with `Figure.add_subfigure` or `Figure.subfigures` methods - (provisional API v3.4). + A logical figure inside a figure, usually added to a figure (or parent `SubFigure`) + with `Figure.add_subfigure` or `Figure.subfigures` methods. Figures are typically created using pyplot methods `~.pyplot.figure`, `~.pyplot.subplots`, and `~.pyplot.subplot_mosaic`. @@ -30,6 +29,7 @@ from contextlib import ExitStack import inspect import itertools +import functools import logging from numbers import Integral import threading @@ -63,8 +63,8 @@ def _stale_figure_callback(self, val): - if self.figure: - self.figure.stale = val + if (fig := self.get_figure(root=False)) is not None: + fig.stale = val class _AxesStack: @@ -194,14 +194,15 @@ def autofmt_xdate( Selects which ticklabels to rotate. """ _api.check_in_list(['major', 'minor', 'both'], which=which) - allsubplots = all(ax.get_subplotspec() for ax in self.axes) - if len(self.axes) == 1: + axes = [ax for ax in self.axes if ax._label != ''] + allsubplots = all(ax.get_subplotspec() for ax in axes) + if len(axes) == 1: for label in self.axes[0].get_xticklabels(which=which): label.set_ha(ha) label.set_rotation(rotation) else: if allsubplots: - for ax in self.get_axes(): + for ax in axes: if ax.get_subplotspec().is_last_row(): for label in ax.get_xticklabels(which=which): label.set_ha(ha) @@ -211,7 +212,8 @@ def autofmt_xdate( label.set_visible(False) ax.set_xlabel('') - if allsubplots: + engine = self.get_layout_engine() + if allsubplots and (engine is None or engine.adjust_compatible): self.subplots_adjust(bottom=bottom) self.stale = True @@ -227,6 +229,67 @@ def get_children(self): *self.legends, *self.subfigs] + def get_figure(self, root=None): + """ + Return the `.Figure` or `.SubFigure` instance the (Sub)Figure belongs to. + + Parameters + ---------- + root : bool, default=True + If False, return the (Sub)Figure this artist is on. If True, + return the root Figure for a nested tree of SubFigures. + + .. deprecated:: 3.10 + + From version 3.12 *root* will default to False. + """ + if self._root_figure is self: + # Top level Figure + return self + + if self._parent is self._root_figure: + # Return early to prevent the deprecation warning when *root* does not + # matter + return self._parent + + if root is None: + # When deprecation expires, consider removing the docstring and just + # inheriting the one from Artist. + message = ('From Matplotlib 3.12 SubFigure.get_figure will by default ' + 'return the direct parent figure, which may be a SubFigure. ' + 'To suppress this warning, pass the root parameter. Pass ' + '`True` to maintain the old behavior and `False` to opt-in to ' + 'the future behavior.') + _api.warn_deprecated('3.10', message=message) + root = True + + if root: + return self._root_figure + + return self._parent + + def set_figure(self, fig): + """ + .. deprecated:: 3.10 + Currently this method will raise an exception if *fig* is anything other + than the root `.Figure` this (Sub)Figure is on. In future it will always + raise an exception. + """ + no_switch = ("The parent and root figures of a (Sub)Figure are set at " + "instantiation and cannot be changed.") + if fig is self._root_figure: + _api.warn_deprecated( + "3.10", + message=(f"{no_switch} From Matplotlib 3.12 this operation will raise " + "an exception.")) + return + + raise ValueError(no_switch) + + figure = property(functools.partial(get_figure, root=True), set_figure, + doc=("The root `Figure`. To get the parent of a `SubFigure`, " + "use the `get_figure` method.")) + def contains(self, mouseevent): """ Test whether the mouse event occurred on the figure. @@ -317,7 +380,7 @@ def _suplabels(self, t, info, **kwargs): self.stale = True return suplab - @_docstring.Substitution(x0=0.5, y0=0.98, name='suptitle', ha='center', + @_docstring.Substitution(x0=0.5, y0=0.98, name='super title', ha='center', va='top', rc='title') @_docstring.copy(_suplabels) def suptitle(self, t, **kwargs): @@ -332,7 +395,7 @@ def get_suptitle(self): text_obj = self._suptitle return "" if text_obj is None else text_obj.get_text() - @_docstring.Substitution(x0=0.5, y0=0.01, name='supxlabel', ha='center', + @_docstring.Substitution(x0=0.5, y0=0.01, name='super xlabel', ha='center', va='bottom', rc='label') @_docstring.copy(_suplabels) def supxlabel(self, t, **kwargs): @@ -347,7 +410,7 @@ def get_supxlabel(self): text_obj = self._supxlabel return "" if text_obj is None else text_obj.get_text() - @_docstring.Substitution(x0=0.02, y0=0.5, name='supylabel', ha='left', + @_docstring.Substitution(x0=0.02, y0=0.5, name='super ylabel', ha='left', va='center', rc='label') @_docstring.copy(_suplabels) def supylabel(self, t, **kwargs): @@ -465,7 +528,7 @@ def add_artist(self, artist, clip=False): self.stale = True return artist - @_docstring.dedent_interpd + @_docstring.interpd def add_axes(self, *args, **kwargs): """ Add an `~.axes.Axes` to the figure. @@ -552,22 +615,22 @@ def add_axes(self, *args, **kwargs): """ if not len(args) and 'rect' not in kwargs: - raise TypeError( - "add_axes() missing 1 required positional argument: 'rect'") + raise TypeError("add_axes() missing 1 required positional argument: 'rect'") elif 'rect' in kwargs: if len(args): - raise TypeError( - "add_axes() got multiple values for argument 'rect'") + raise TypeError("add_axes() got multiple values for argument 'rect'") args = (kwargs.pop('rect'), ) + if len(args) != 1: + raise _api.nargs_error("add_axes", 1, len(args)) if isinstance(args[0], Axes): - a, *extra_args = args + a, = args key = a._projection_init - if a.get_figure() is not self: + if a.get_figure(root=False) is not self: raise ValueError( "The Axes must have been created in the present figure") else: - rect, *extra_args = args + rect, = args if not np.isfinite(rect).all(): raise ValueError(f'all entries in rect must be finite not {rect}') projection_class, pkw = self._process_projection_requirements(**kwargs) @@ -576,14 +639,9 @@ def add_axes(self, *args, **kwargs): a = projection_class(self, rect, **pkw) key = (projection_class, pkw) - if extra_args: - _api.warn_deprecated( - "3.8", - name="Passing more than one positional argument to Figure.add_axes", - addendum="Any additional positional arguments are currently ignored.") return self._add_axes_internal(a, key) - @_docstring.dedent_interpd + @_docstring.interpd def add_subplot(self, *args, **kwargs): """ Add an `~.axes.Axes` to the figure as part of a subplot arrangement. @@ -694,7 +752,7 @@ def add_subplot(self, *args, **kwargs): and args[0].get_subplotspec()): ax = args[0] key = ax._projection_init - if ax.get_figure() is not self: + if ax.get_figure(root=False) is not self: raise ValueError("The Axes must have been created in " "the present figure") else: @@ -885,7 +943,7 @@ def _remove_axes(self, ax, owners): self._axobservers.process("_axes_change_event", self) self.stale = True - self.canvas.release_mouse(ax) + self._root_figure.canvas.release_mouse(ax) for name in ax._axis_names: # Break link between any shared Axes grouper = ax._shared_axes[name] @@ -962,7 +1020,7 @@ def clf(self, keep_observers=False): # " legend(" -> " figlegend(" for the signatures # "fig.legend(" -> "plt.figlegend" for the code examples # "ax.plot" -> "plt.plot" for consistency in using pyplot when able - @_docstring.dedent_interpd + @_docstring.interpd def legend(self, *args, **kwargs): """ Place a legend on the figure. @@ -1082,7 +1140,7 @@ def legend(self, *args, **kwargs): self.stale = True return l - @_docstring.dedent_interpd + @_docstring.interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to figure. @@ -1132,7 +1190,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): self.stale = True return text - @_docstring.dedent_interpd + @_docstring.interpd def colorbar( self, mappable, cax=None, ax=None, use_gridspec=True, **kwargs): """ @@ -1220,7 +1278,7 @@ def colorbar( fig = ( # Figure of first Axes; logic copied from make_axes. [*ax.flat] if isinstance(ax, np.ndarray) else [*ax] if np.iterable(ax) - else [ax])[0].figure + else [ax])[0].get_figure(root=False) current_ax = fig.gca() if (fig.get_layout_engine() is not None and not fig.get_layout_engine().colorbar_gridspec): @@ -1235,24 +1293,21 @@ def colorbar( fig.sca(current_ax) cax.grid(visible=False, which='both', axis='both') - if hasattr(mappable, "figure") and mappable.figure is not None: - # Get top level artists - mappable_host_fig = mappable.figure - if isinstance(mappable_host_fig, mpl.figure.SubFigure): - mappable_host_fig = mappable_host_fig.figure + if (hasattr(mappable, "get_figure") and + (mappable_host_fig := mappable.get_figure(root=True)) is not None): # Warn in case of mismatch - if mappable_host_fig is not self.figure: + if mappable_host_fig is not self._root_figure: _api.warn_external( f'Adding colorbar to a different Figure ' - f'{repr(mappable.figure)} than ' - f'{repr(self.figure)} which ' + f'{repr(mappable_host_fig)} than ' + f'{repr(self._root_figure)} which ' f'fig.colorbar is called on.') NON_COLORBAR_KEYS = [ # remove kws that cannot be passed to Colorbar 'fraction', 'pad', 'shrink', 'aspect', 'anchor', 'panchor'] cb = cbar.Colorbar(cax, mappable, **{ k: v for k, v in kwargs.items() if k not in NON_COLORBAR_KEYS}) - cax.figure.stale = True + cax.get_figure(root=False).stale = True return cb def subplots_adjust(self, left=None, bottom=None, right=None, top=None, @@ -1263,6 +1318,8 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, Unset parameters are left unmodified; initial values are given by :rc:`figure.subplot.[name]`. + .. plot:: _embedded_plots/figure_subplots_adjust.py + Parameters ---------- left : float, optional @@ -1325,8 +1382,8 @@ def align_xlabels(self, axs=None): Notes ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. + This assumes that all Axes in ``axs`` are from the same `.GridSpec`, + so that their `.SubplotSpec` positions correspond to figure positions. Examples -------- @@ -1387,8 +1444,8 @@ def align_ylabels(self, axs=None): Notes ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. + This assumes that all Axes in ``axs`` are from the same `.GridSpec`, + so that their `.SubplotSpec` positions correspond to figure positions. Examples -------- @@ -1443,8 +1500,8 @@ def align_titles(self, axs=None): Notes ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. + This assumes that all Axes in ``axs`` are from the same `.GridSpec`, + so that their `.SubplotSpec` positions correspond to figure positions. Examples -------- @@ -1487,6 +1544,11 @@ def align_labels(self, axs=None): matplotlib.figure.Figure.align_xlabels matplotlib.figure.Figure.align_ylabels matplotlib.figure.Figure.align_titles + + Notes + ----- + This assumes that all Axes in ``axs`` are from the same `.GridSpec`, + so that their `.SubplotSpec` positions correspond to figure positions. """ self.align_xlabels(axs=axs) self.align_ylabels(axs=axs) @@ -1549,8 +1611,8 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, the same as a figure, but cannot print itself. See :doc:`/gallery/subplots_axes_and_figures/subfigures`. - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. + .. versionchanged:: 3.10 + subfigures are now added in row-major order. Parameters ---------- @@ -1585,9 +1647,9 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, left=0, right=1, bottom=0, top=1) sfarr = np.empty((nrows, ncols), dtype=object) - for i in range(ncols): - for j in range(nrows): - sfarr[j, i] = self.add_subfigure(gs[j, i], **kwargs) + for i in range(nrows): + for j in range(ncols): + sfarr[i, j] = self.add_subfigure(gs[i, j], **kwargs) if self.get_layout_engine() is None and (wspace is not None or hspace is not None): @@ -1735,8 +1797,7 @@ def get_default_bbox_extra_artists(self): bbox_artists.extend(ax.get_default_bbox_extra_artists()) return bbox_artists - @_api.make_keyword_only("3.8", "bbox_extra_artists") - def get_tightbbox(self, renderer=None, bbox_extra_artists=None): + def get_tightbbox(self, renderer=None, *, bbox_extra_artists=None): """ Return a (tight) bounding box of the figure *in inches*. @@ -1764,7 +1825,7 @@ def get_tightbbox(self, renderer=None, bbox_extra_artists=None): """ if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() bb = [] if bbox_extra_artists is None: @@ -2168,9 +2229,6 @@ class SubFigure(FigureBase): axsR = sfigs[1].subplots(2, 1) See :doc:`/gallery/subplots_axes_and_figures/subfigures` - - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. """ def __init__(self, parent, subplotspec, *, @@ -2219,7 +2277,7 @@ def __init__(self, parent, subplotspec, *, self._subplotspec = subplotspec self._parent = parent - self.figure = parent.figure + self._root_figure = parent._root_figure # subfigures use the parent axstack self._axstack = parent._axstack @@ -2356,7 +2414,7 @@ def draw(self, renderer): renderer.open_group('subfigure', gid=self.get_gid()) self.patch.draw(renderer) mimage._draw_list_compositing_images( - renderer, self, artists, self.figure.suppressComposite) + renderer, self, artists, self.get_figure(root=True).suppressComposite) renderer.close_group('subfigure') finally: @@ -2500,7 +2558,7 @@ def __init__(self, %(Figure:kwdoc)s """ super().__init__(**kwargs) - self.figure = self + self._root_figure = self self._layout_engine = None if layout is not None: @@ -2757,6 +2815,36 @@ def axes(self): get_axes = axes.fget + @property + def number(self): + """The figure id, used to identify figures in `.pyplot`.""" + # Historically, pyplot dynamically added a number attribute to figure. + # However, this number must stay in sync with the figure manager. + # AFAICS overwriting the number attribute does not have the desired + # effect for pyplot. But there are some repos in GitHub that do change + # number. So let's take it slow and properly migrate away from writing. + # + # Making the dynamic attribute private and wrapping it in a property + # allows to maintain current behavior and deprecate write-access. + # + # When the deprecation expires, there's no need for duplicate state + # anymore and the private _number attribute can be replaced by + # `self.canvas.manager.num` if that exists and None otherwise. + if hasattr(self, '_number'): + return self._number + else: + raise AttributeError( + "'Figure' object has no attribute 'number'. In the future this" + "will change to returning 'None' instead.") + + @number.setter + def number(self, num): + _api.warn_deprecated( + "3.10", + message="Changing 'Figure.number' is deprecated since %(since)s and " + "will raise an error starting %(removal)s") + self._number = num + def _get_renderer(self): if hasattr(self.canvas, 'get_renderer'): return self.canvas.get_renderer() @@ -2921,7 +3009,8 @@ def set_canvas(self, canvas): @_docstring.interpd def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, - vmin=None, vmax=None, origin=None, resize=False, **kwargs): + vmin=None, vmax=None, origin=None, resize=False, *, + colorizer=None, **kwargs): """ Add a non-resampled image to the figure. @@ -2964,6 +3053,10 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, resize : bool If *True*, resize the figure to match the given image size. + %(colorizer_doc)s + + This parameter is ignored if *X* is RGB(A). + Returns ------- `matplotlib.image.FigureImage` @@ -2997,6 +3090,7 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, self.set_size_inches(figsize, forward=True) im = mimage.FigureImage(self, cmap=cmap, norm=norm, + colorizer=colorizer, offsetx=xo, offsety=yo, origin=origin, **kwargs) im.stale_callback = _stale_figure_callback @@ -3004,6 +3098,7 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, im.set_array(X) im.set_alpha(alpha) if norm is None: + im._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) im.set_clim(vmin, vmax) self.images.append(im) im._remove_method = self.images.remove diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index c31f90b4b2a8..08bf1505532b 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -1,6 +1,6 @@ -from collections.abc import Callable, Hashable, Iterable +from collections.abc import Callable, Hashable, Iterable, Sequence import os -from typing import Any, IO, Literal, Sequence, TypeVar, overload +from typing import Any, IO, Literal, TypeVar, overload import numpy as np from numpy.typing import ArrayLike @@ -15,6 +15,7 @@ from matplotlib.backend_bases import ( ) from matplotlib.colors import Colormap, Normalize from matplotlib.colorbar import Colorbar +from matplotlib.colorizer import ColorizingArtist, Colorizer from matplotlib.cm import ScalarMappable from matplotlib.gridspec import GridSpec, SubplotSpec, SubplotParams as SubplotParams from matplotlib.image import _ImageBase, FigureImage @@ -61,6 +62,12 @@ class FigureBase(Artist): def get_linewidth(self) -> float: ... def set_edgecolor(self, color: ColorType) -> None: ... def set_facecolor(self, color: ColorType) -> None: ... + @overload + def get_figure(self, root: Literal[True]) -> Figure: ... + @overload + def get_figure(self, root: Literal[False]) -> Figure | SubFigure: ... + @overload + def get_figure(self, root: bool = ...) -> Figure | SubFigure: ... def set_frameon(self, b: bool) -> None: ... @property def frameon(self) -> bool: ... @@ -158,7 +165,7 @@ class FigureBase(Artist): ) -> Text: ... def colorbar( self, - mappable: ScalarMappable, + mappable: ScalarMappable | ColorizingArtist, cax: Axes | None = ..., ax: Axes | Iterable[Axes] | None = ..., use_gridspec: bool = ..., @@ -205,7 +212,7 @@ class FigureBase(Artist): def add_subfigure(self, subplotspec: SubplotSpec, **kwargs) -> SubFigure: ... def sca(self, a: Axes) -> Axes: ... def gca(self) -> Axes: ... - def _gci(self) -> ScalarMappable | None: ... + def _gci(self) -> ColorizingArtist | None: ... def _process_projection_requirements( self, *, axes_class=None, polar=False, projection=None, **kwargs ) -> tuple[type[Axes], dict[str, Any]]: ... @@ -260,7 +267,8 @@ class FigureBase(Artist): ) -> dict[Hashable, Axes]: ... class SubFigure(FigureBase): - figure: Figure + @property + def figure(self) -> Figure: ... subplotpars: SubplotParams dpi_scale_trans: Affine2D transFigure: Transform @@ -298,7 +306,8 @@ class SubFigure(FigureBase): def get_axes(self) -> list[Axes]: ... class Figure(FigureBase): - figure: Figure + @property + def figure(self) -> Figure: ... bbox_inches: Bbox dpi_scale_trans: Affine2D bbox: BboxBase @@ -335,6 +344,10 @@ class Figure(FigureBase): def get_layout_engine(self) -> LayoutEngine | None: ... def _repr_html_(self) -> str | None: ... def show(self, warn: bool = ...) -> None: ... + @property + def number(self) -> int | str: ... + @number.setter + def number(self, num: int | str) -> None: ... @property # type: ignore[misc] def axes(self) -> list[Axes]: ... # type: ignore[override] def get_axes(self) -> list[Axes]: ... @@ -361,6 +374,8 @@ class Figure(FigureBase): vmax: float | None = ..., origin: Literal["upper", "lower"] | None = ..., resize: bool = ..., + *, + colorizer: Colorizer | None = ..., **kwargs ) -> FigureImage: ... def set_size_inches( diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d9560ec0cc0f..9aa8dccde444 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -28,10 +28,10 @@ from __future__ import annotations from base64 import b64encode -from collections import namedtuple import copy import dataclasses from functools import lru_cache +import functools from io import BytesIO import json import logging @@ -132,8 +132,6 @@ 'sans', } -_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message']) - # OS Font paths try: _HOME = Path.home() @@ -381,7 +379,7 @@ def ttfFontProperty(font): style = 'italic' elif sfnt2.find('regular') >= 0: style = 'normal' - elif font.style_flags & ft2font.ITALIC: + elif ft2font.StyleFlags.ITALIC in font.style_flags: style = 'italic' else: style = 'normal' @@ -430,7 +428,7 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. for regex, weight in _weight_regexes: if re.search(regex, style, re.I): return weight - if font.style_flags & ft2font.BOLD: + if ft2font.StyleFlags.BOLD in font.style_flags: return 700 # "bold" return 500 # "medium", not "regular"! @@ -536,6 +534,57 @@ def afmFontProperty(fontpath, font): return FontEntry(fontpath, name, style, variant, weight, stretch, size) +def _cleanup_fontproperties_init(init_method): + """ + A decorator to limit the call signature to single a positional argument + or alternatively only keyword arguments. + + We still accept but deprecate all other call signatures. + + When the deprecation expires we can switch the signature to:: + + __init__(self, pattern=None, /, *, family=None, style=None, ...) + + plus a runtime check that pattern is not used alongside with the + keyword arguments. This results eventually in the two possible + call signatures:: + + FontProperties(pattern) + FontProperties(family=..., size=..., ...) + + """ + @functools.wraps(init_method) + def wrapper(self, *args, **kwargs): + # multiple args with at least some positional ones + if len(args) > 1 or len(args) == 1 and kwargs: + # Note: Both cases were previously handled as individual properties. + # Therefore, we do not mention the case of font properties here. + _api.warn_deprecated( + "3.10", + message="Passing individual properties to FontProperties() " + "positionally was deprecated in Matplotlib %(since)s and " + "will be removed in %(removal)s. Please pass all properties " + "via keyword arguments." + ) + # single non-string arg -> clearly a family not a pattern + if len(args) == 1 and not kwargs and not cbook.is_scalar_or_string(args[0]): + # Case font-family list passed as single argument + _api.warn_deprecated( + "3.10", + message="Passing family as positional argument to FontProperties() " + "was deprecated in Matplotlib %(since)s and will be removed " + "in %(removal)s. Please pass family names as keyword" + "argument." + ) + # Note on single string arg: + # This has been interpreted as pattern so far. We are already raising if a + # non-pattern compatible family string was given. Therefore, we do not need + # to warn for this case. + return init_method(self, *args, **kwargs) + + return wrapper + + class FontProperties: """ A class for storing and manipulating font properties. @@ -585,9 +634,14 @@ class FontProperties: approach allows all text sizes to be made larger or smaller based on the font manager's default font size. - This class will also accept a fontconfig_ pattern_, if it is the only - argument provided. This support does not depend on fontconfig; we are - merely borrowing its pattern syntax for use here. + This class accepts a single positional string as fontconfig_ pattern_, + or alternatively individual properties as keyword arguments:: + + FontProperties(pattern) + FontProperties(*, family=None, style=None, variant=None, ...) + + This support does not depend on fontconfig; we are merely borrowing its + pattern syntax for use here. .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/ .. _pattern: @@ -599,6 +653,7 @@ class FontProperties: fontconfig. """ + @_cleanup_fontproperties_init def __init__(self, family=None, style=None, variant=None, weight=None, stretch=None, size=None, fname=None, # if set, it's a hardcoded filename to use @@ -1297,8 +1352,8 @@ def findfont(self, prop, fontext='ttf', directory=None, ret = self._findfont_cached( prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) - if isinstance(ret, _ExceptionProxy): - raise ret.klass(ret.message) + if isinstance(ret, cbook._ExceptionInfo): + raise ret.to_exception() return ret def get_font_names(self): @@ -1451,7 +1506,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy( + return cbook._ExceptionInfo( ValueError, f"Failed to find font {prop}, and fallback to the default font was " f"disabled" @@ -1477,7 +1532,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy(ValueError, "No valid font could be found") + return cbook._ExceptionInfo(ValueError, "No valid font could be found") return _cached_realpath(result) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 6a0716e993a5..1638bac692d3 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,46 +1,71 @@ -from typing import BinaryIO, Literal, TypedDict, overload +from enum import Enum, Flag +import sys +from typing import BinaryIO, Literal, TypedDict, final, overload +from typing_extensions import Buffer # < Py 3.12 import numpy as np from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str -BOLD: int -EXTERNAL_STREAM: int -FAST_GLYPHS: int -FIXED_SIZES: int -FIXED_WIDTH: int -GLYPH_NAMES: int -HORIZONTAL: int -ITALIC: int -KERNING: int -KERNING_DEFAULT: int -KERNING_UNFITTED: int -KERNING_UNSCALED: int -LOAD_CROP_BITMAP: int -LOAD_DEFAULT: int -LOAD_FORCE_AUTOHINT: int -LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH: int -LOAD_IGNORE_TRANSFORM: int -LOAD_LINEAR_DESIGN: int -LOAD_MONOCHROME: int -LOAD_NO_AUTOHINT: int -LOAD_NO_BITMAP: int -LOAD_NO_HINTING: int -LOAD_NO_RECURSE: int -LOAD_NO_SCALE: int -LOAD_PEDANTIC: int -LOAD_RENDER: int -LOAD_TARGET_LCD: int -LOAD_TARGET_LCD_V: int -LOAD_TARGET_LIGHT: int -LOAD_TARGET_MONO: int -LOAD_TARGET_NORMAL: int -LOAD_VERTICAL_LAYOUT: int -MULTIPLE_MASTERS: int -SCALABLE: int -SFNT: int -VERTICAL: int + +class FaceFlags(Flag): + SCALABLE: int + FIXED_SIZES: int + FIXED_WIDTH: int + SFNT: int + HORIZONTAL: int + VERTICAL: int + KERNING: int + FAST_GLYPHS: int + MULTIPLE_MASTERS: int + GLYPH_NAMES: int + EXTERNAL_STREAM: int + HINTER: int + CID_KEYED: int + TRICKY: int + COLOR: int + # VARIATION: int # FT 2.9 + # SVG: int # FT 2.12 + # SBIX: int # FT 2.12 + # SBIX_OVERLAY: int # FT 2.12 + +class Kerning(Enum): + DEFAULT: int + UNFITTED: int + UNSCALED: int + +class LoadFlags(Flag): + DEFAULT: int + NO_SCALE: int + NO_HINTING: int + RENDER: int + NO_BITMAP: int + VERTICAL_LAYOUT: int + FORCE_AUTOHINT: int + CROP_BITMAP: int + PEDANTIC: int + IGNORE_GLOBAL_ADVANCE_WIDTH: int + NO_RECURSE: int + IGNORE_TRANSFORM: int + MONOCHROME: int + LINEAR_DESIGN: int + NO_AUTOHINT: int + COLOR: int + COMPUTE_METRICS: int # FT 2.6.1 + # BITMAP_METRICS_ONLY: int # FT 2.7.1 + # NO_SVG: int # FT 2.13.1 + # The following should be unique, but the above can be OR'd together. + TARGET_NORMAL: int + TARGET_LIGHT: int + TARGET_MONO: int + TARGET_LCD: int + TARGET_LCD_V: int + +class StyleFlags(Flag): + NORMAL: int + ITALIC: int + BOLD: int class _SfntHeadDict(TypedDict): version: tuple[int, int] @@ -158,28 +183,8 @@ class _SfntPcltDict(TypedDict): widthType: int serifStyle: int -class FT2Font: - ascender: int - bbox: tuple[int, int, int, int] - descender: int - face_flags: int - family_name: str - fname: str - height: int - max_advance_height: int - max_advance_width: int - num_charmaps: int - num_faces: int - num_fixed_sizes: int - num_glyphs: int - postscript_name: str - scalable: bool - style_flags: int - style_name: str - underline_position: int - underline_thickness: int - units_per_EM: int - +@final +class FT2Font(Buffer): def __init__( self, filename: str | BinaryIO, @@ -188,10 +193,12 @@ class FT2Font: _fallback_list: list[FT2Font] | None = ..., _kerning_factor: int = ... ) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( - self, image: FT2Image, x: float, y: float, glyph: Glyph, antialiased: bool = ... + self, image: FT2Image, x: int, y: int, glyph: Glyph, antialiased: bool = ... ) -> None: ... def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ... def get_bitmap_offset(self) -> tuple[int, int]: ... @@ -200,7 +207,7 @@ class FT2Font: def get_descent(self) -> int: ... def get_glyph_name(self, index: int) -> str: ... def get_image(self) -> NDArray[np.uint8]: ... - def get_kerning(self, left: int, right: int, mode: int) -> int: ... + def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ... def get_name_index(self, name: str) -> int: ... def get_num_glyphs(self) -> int: ... def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ... @@ -223,31 +230,81 @@ class FT2Font: @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def get_xys(self, antialiased: bool = ...) -> NDArray[np.float64]: ... - def load_char(self, charcode: int, flags: int = ...) -> Glyph: ... - def load_glyph(self, glyphindex: int, flags: int = ...) -> Glyph: ... + def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ... + def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... def set_text( - self, string: str, angle: float = ..., flags: int = ... + self, string: str, angle: float = ..., flags: LoadFlags = ... ) -> NDArray[np.float64]: ... + @property + def ascender(self) -> int: ... + @property + def bbox(self) -> tuple[int, int, int, int]: ... + @property + def descender(self) -> int: ... + @property + def face_flags(self) -> FaceFlags: ... + @property + def family_name(self) -> str: ... + @property + def fname(self) -> str: ... + @property + def height(self) -> int: ... + @property + def max_advance_height(self) -> int: ... + @property + def max_advance_width(self) -> int: ... + @property + def num_charmaps(self) -> int: ... + @property + def num_faces(self) -> int: ... + @property + def num_fixed_sizes(self) -> int: ... + @property + def num_glyphs(self) -> int: ... + @property + def postscript_name(self) -> str: ... + @property + def scalable(self) -> bool: ... + @property + def style_flags(self) -> StyleFlags: ... + @property + def style_name(self) -> str: ... + @property + def underline_position(self) -> int: ... + @property + def underline_thickness(self) -> int: ... + @property + def units_per_EM(self) -> int: ... -class FT2Image: # TODO: When updating mypy>=1.4, subclass from Buffer. - def __init__(self, width: float, height: float) -> None: ... - def draw_rect(self, x0: float, y0: float, x1: float, y1: float) -> None: ... - def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... +@final +class FT2Image(Buffer): + def __init__(self, width: int, height: int) -> None: ... + def draw_rect_filled(self, x0: int, y0: int, x1: int, y1: int) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... +@final class Glyph: - width: int - height: int - horiBearingX: int - horiBearingY: int - horiAdvance: int - linearHoriAdvance: int - vertBearingX: int - vertBearingY: int - vertAdvance: int - + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def horiBearingX(self) -> int: ... + @property + def horiBearingY(self) -> int: ... + @property + def horiAdvance(self) -> int: ... + @property + def linearHoriAdvance(self) -> int: ... + @property + def vertBearingX(self) -> int: ... + @property + def vertBearingY(self) -> int: ... + @property + def vertAdvance(self) -> int: ... @property def bbox(self) -> tuple[int, int, int, int]: ... diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index c6b363d36efa..06f0b2f7f781 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -391,8 +391,8 @@ def update(self, **kwargs): if ax.get_subplotspec() is not None: ss = ax.get_subplotspec().get_topmost_subplotspec() if ss.get_gridspec() == self: - ax._set_position( - ax.get_subplotspec().get_position(ax.figure)) + fig = ax.get_figure(root=False) + ax._set_position(ax.get_subplotspec().get_position(fig)) def get_subplot_params(self, figure=None): """ diff --git a/lib/matplotlib/gridspec.pyi b/lib/matplotlib/gridspec.pyi index b6732ad8fafa..08c4dd7f4e49 100644 --- a/lib/matplotlib/gridspec.pyi +++ b/lib/matplotlib/gridspec.pyi @@ -3,7 +3,7 @@ from typing import Any, Literal, overload from numpy.typing import ArrayLike import numpy as np -from matplotlib.axes import Axes, SubplotBase +from matplotlib.axes import Axes from matplotlib.backend_bases import RendererBase from matplotlib.figure import Figure from matplotlib.transforms import Bbox diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 7a4b283c1dbe..0cbd042e1628 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -192,7 +192,7 @@ def _validate_hatch_pattern(hatch): message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' - 'since %(since)s and will become an error %(removal)s.' + 'since %(since)s and will become an error in %(removal)s.' ) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 2e13293028ca..37a1d11678fa 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -14,13 +14,14 @@ import PIL.PngImagePlugin import matplotlib as mpl -from matplotlib import _api, cbook, cm +from matplotlib import _api, cbook # For clarity, names from _image are given explicitly in this module from matplotlib import _image # For user convenience, the names from _image are also imported into # the image namespace from matplotlib._image import * # noqa: F401, F403 import matplotlib.artist as martist +import matplotlib.colorizer as mcolorizer from matplotlib.backend_bases import FigureCanvasBase import matplotlib.colors as mcolors from matplotlib.transforms import ( @@ -31,7 +32,7 @@ # map interpolation strings to module constants _interpd_ = { - 'antialiased': _image.NEAREST, # this will use nearest or Hanning... + 'auto': _image.NEAREST, # this will use nearest or Hanning... 'none': _image.NEAREST, # fall back to nearest when not supported 'nearest': _image.NEAREST, 'bilinear': _image.BILINEAR, @@ -50,6 +51,7 @@ 'sinc': _image.SINC, 'lanczos': _image.LANCZOS, 'blackman': _image.BLACKMAN, + 'antialiased': _image.NEAREST, # this will use nearest or Hanning... } interpolations_names = set(_interpd_) @@ -186,7 +188,7 @@ def _resample( # compare the number of displayed pixels to the number of # the data pixels. interpolation = image_obj.get_interpolation() - if interpolation == 'antialiased': + if interpolation in ['antialiased', 'auto']: # don't antialias if upsampling by an integer number or # if zooming in more than a factor of 3 pos = np.array([[0, 0], [data.shape[1], data.shape[0]]]) @@ -228,7 +230,7 @@ def _rgb_to_rgba(A): return rgba -class _ImageBase(martist.Artist, cm.ScalarMappable): +class _ImageBase(mcolorizer.ColorizingArtist): """ Base class for images. @@ -248,6 +250,7 @@ class _ImageBase(martist.Artist, cm.ScalarMappable): def __init__(self, ax, cmap=None, norm=None, + colorizer=None, interpolation=None, origin=None, filternorm=True, @@ -257,8 +260,7 @@ def __init__(self, ax, interpolation_stage=None, **kwargs ): - martist.Artist.__init__(self) - cm.ScalarMappable.__init__(self, norm, cmap) + super().__init__(self._get_colorizer(cmap, norm, colorizer)) if origin is None: origin = mpl.rcParams['image.origin'] _api.check_in_list(["upper", "lower"], origin=origin) @@ -330,7 +332,7 @@ def changed(self): Call this whenever the mappable is changed so observers can update. """ self._imcache = None - cm.ScalarMappable.changed(self) + super().changed() def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, unsampled=False, round_to_pixel_border=True): @@ -340,18 +342,33 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, the given *clip_bbox* (also in pixel space), and magnified by the *magnification* factor. - *A* may be a greyscale image (M, N) with a dtype of `~numpy.float32`, - `~numpy.float64`, `~numpy.float128`, `~numpy.uint16` or `~numpy.uint8`, - or an (M, N, 4) RGBA image with a dtype of `~numpy.float32`, - `~numpy.float64`, `~numpy.float128`, or `~numpy.uint8`. + Parameters + ---------- + A : ndarray - If *unsampled* is True, the image will not be scaled, but an - appropriate affine transformation will be returned instead. + - a (M, N) array interpreted as scalar (greyscale) image, + with one of the dtypes `~numpy.float32`, `~numpy.float64`, + `~numpy.float128`, `~numpy.uint16` or `~numpy.uint8`. + - (M, N, 4) RGBA image with a dtype of `~numpy.float32`, + `~numpy.float64`, `~numpy.float128`, or `~numpy.uint8`. - If *round_to_pixel_border* is True, the output image size will be - rounded to the nearest pixel boundary. This makes the images align - correctly with the Axes. It should not be used if exact scaling is - needed, such as for `FigureImage`. + in_bbox : `~matplotlib.transforms.Bbox` + + out_bbox : `~matplotlib.transforms.Bbox` + + clip_bbox : `~matplotlib.transforms.Bbox` + + magnification : float, default: 1 + + unsampled : bool, default: False + If True, the image will not be scaled, but an appropriate + affine transformation will be returned instead. + + round_to_pixel_border : bool, default: True + If True, the output image size will be rounded to the nearest pixel + boundary. This makes the images align correctly with the Axes. + It should not be used if exact scaling is needed, such as for + `.FigureImage`. Returns ------- @@ -421,103 +438,49 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if not unsampled: if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)): raise ValueError(f"Invalid shape {A.shape} for image data") - if A.ndim == 2 and self._interpolation_stage != 'rgba': + + # if antialiased, this needs to change as window sizes + # change: + interpolation_stage = self._interpolation_stage + if interpolation_stage in ['antialiased', 'auto']: + pos = np.array([[0, 0], [A.shape[1], A.shape[0]]]) + disp = t.transform(pos) + dispx = np.abs(np.diff(disp[:, 0])) / A.shape[1] + dispy = np.abs(np.diff(disp[:, 1])) / A.shape[0] + if (dispx < 3) or (dispy < 3): + interpolation_stage = 'rgba' + else: + interpolation_stage = 'data' + + if A.ndim == 2 and interpolation_stage == 'data': # if we are a 2D array, then we are running through the # norm + colormap transformation. However, in general the # input data is not going to match the size on the screen so we # have to resample to the correct number of pixels - # TODO slice input array first - a_min = A.min() - a_max = A.max() - if a_min is np.ma.masked: # All masked; values don't matter. - a_min, a_max = np.int32(0), np.int32(1) if A.dtype.kind == 'f': # Float dtype: scale to same dtype. - scaled_dtype = np.dtype( - np.float64 if A.dtype.itemsize > 4 else np.float32) + scaled_dtype = np.dtype("f8" if A.dtype.itemsize > 4 else "f4") if scaled_dtype.itemsize < A.dtype.itemsize: _api.warn_external(f"Casting input data from {A.dtype}" f" to {scaled_dtype} for imshow.") else: # Int dtype, likely. + # TODO slice input array first # Scale to appropriately sized float: use float32 if the # dynamic range is small, to limit the memory footprint. - da = a_max.astype(np.float64) - a_min.astype(np.float64) - scaled_dtype = np.float64 if da > 1e8 else np.float32 - - # Scale the input data to [.1, .9]. The Agg interpolators clip - # to [0, 1] internally, and we use a smaller input scale to - # identify the interpolated points that need to be flagged as - # over/under. This may introduce numeric instabilities in very - # broadly scaled data. - - # Always copy, and don't allow array subtypes. - A_scaled = np.array(A, dtype=scaled_dtype) - # Clip scaled data around norm if necessary. This is necessary - # for big numbers at the edge of float64's ability to represent - # changes. Applying a norm first would be good, but ruins the - # interpolation of over numbers. - self.norm.autoscale_None(A) - dv = np.float64(self.norm.vmax) - np.float64(self.norm.vmin) - vmid = np.float64(self.norm.vmin) + dv / 2 - fact = 1e7 if scaled_dtype == np.float64 else 1e4 - newmin = vmid - dv * fact - if newmin < a_min: - newmin = None - else: - a_min = np.float64(newmin) - newmax = vmid + dv * fact - if newmax > a_max: - newmax = None - else: - a_max = np.float64(newmax) - if newmax is not None or newmin is not None: - np.clip(A_scaled, newmin, newmax, out=A_scaled) - - # Rescale the raw data to [offset, 1-offset] so that the - # resampling code will run cleanly. Using dyadic numbers here - # could reduce the error, but would not fully eliminate it and - # breaks a number of tests (due to the slightly different - # error bouncing some pixels across a boundary in the (very - # quantized) colormapping step). - offset = .1 - frac = .8 - # Run vmin/vmax through the same rescaling as the raw data; - # otherwise, data values close or equal to the boundaries can - # end up on the wrong side due to floating point error. - vmin, vmax = self.norm.vmin, self.norm.vmax - if vmin is np.ma.masked: - vmin, vmax = a_min, a_max - vrange = np.array([vmin, vmax], dtype=scaled_dtype) - - A_scaled -= a_min - vrange -= a_min - # .item() handles a_min/a_max being ndarray subclasses. - a_min = a_min.astype(scaled_dtype).item() - a_max = a_max.astype(scaled_dtype).item() - - if a_min != a_max: - A_scaled /= ((a_max - a_min) / frac) - vrange /= ((a_max - a_min) / frac) - A_scaled += offset - vrange += offset + da = A.max().astype("f8") - A.min().astype("f8") + scaled_dtype = "f8" if da > 1e8 else "f4" + # resample the input data to the correct resolution and shape - A_resampled = _resample(self, A_scaled, out_shape, t) - del A_scaled # Make sure we don't use A_scaled anymore! - # Un-scale the resampled data to approximately the original - # range. Things that interpolated to outside the original range - # will still be outside, but possibly clipped in the case of - # higher order interpolation + drastically changing data. - A_resampled -= offset - vrange -= offset - if a_min != a_max: - A_resampled *= ((a_max - a_min) / frac) - vrange *= ((a_max - a_min) / frac) - A_resampled += a_min - vrange += a_min + A_resampled = _resample(self, A.astype(scaled_dtype), out_shape, t) + # if using NoNorm, cast back to the original datatype if isinstance(self.norm, mcolors.NoNorm): A_resampled = A_resampled.astype(A.dtype) + # Compute out_mask (what screen pixels include "bad" data + # pixels) and out_alpha (to what extent screen pixels are + # covered by data pixels: 0 outside the data extent, 1 inside + # (even for bad data), and intermediate values at the edges). mask = (np.where(A.mask, np.float32(np.nan), np.float32(1)) if A.mask.shape == A.shape # nontrivial mask else np.ones_like(A, np.float32)) @@ -525,34 +488,17 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # non-affine transformations out_alpha = _resample(self, mask, out_shape, t, resample=True) del mask # Make sure we don't use mask anymore! - # Agg updates out_alpha in place. If the pixel has no image - # data it will not be updated (and still be 0 as we initialized - # it), if input data that would go into that output pixel than - # it will be `nan`, if all the input data for a pixel is good - # it will be 1, and if there is _some_ good data in that output - # pixel it will be between [0, 1] (such as a rotated image). out_mask = np.isnan(out_alpha) out_alpha[out_mask] = 1 # Apply the pixel-by-pixel alpha values if present alpha = self.get_alpha() if alpha is not None and np.ndim(alpha) > 0: - out_alpha *= _resample(self, alpha, out_shape, - t, resample=True) + out_alpha *= _resample(self, alpha, out_shape, t, resample=True) # mask and run through the norm resampled_masked = np.ma.masked_array(A_resampled, out_mask) - # we have re-set the vmin/vmax to account for small errors - # that may have moved input values in/out of range - s_vmin, s_vmax = vrange - if isinstance(self.norm, mcolors.LogNorm) and s_vmin <= 0: - # Don't give 0 or negative values to LogNorm - s_vmin = np.finfo(scaled_dtype).eps - # Block the norm from sending an update signal during the - # temporary vmin/vmax change - with self.norm.callbacks.blocked(), \ - cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax): - output = self.norm(resampled_masked) + output = self.norm(resampled_masked) else: - if A.ndim == 2: # _interpolation_stage == 'rgba' + if A.ndim == 2: # interpolation_stage = 'rgba' self.norm.autoscale_None(A) A = self.to_rgba(A) alpha = self._get_scalar_alpha() @@ -746,9 +692,9 @@ def get_interpolation(self): """ Return the interpolation method the image uses when resizing. - One of 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', - 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', - 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', + One of 'auto', 'antialiased', 'nearest', 'bilinear', 'bicubic', + 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', + 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', or 'none'. """ return self._interpolation @@ -764,7 +710,7 @@ def set_interpolation(self, s): Parameters ---------- - s : {'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', \ + s : {'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', \ 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', \ 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'none'} or None """ @@ -777,7 +723,7 @@ def get_interpolation_stage(self): """ Return when interpolation happens during the transform to RGBA. - One of 'data', 'rgba'. + One of 'data', 'rgba', 'auto'. """ return self._interpolation_stage @@ -787,12 +733,14 @@ def set_interpolation_stage(self, s): Parameters ---------- - s : {'data', 'rgba'} or None + s : {'data', 'rgba', 'auto'} or None Whether to apply up/downsampling interpolation in data or RGBA space. If None, use :rc:`image.interpolation_stage`. + If 'auto' we will check upsampling rate and if less + than 3 then use 'rgba', otherwise use 'data'. """ s = mpl._val_or_rc(s, 'image.interpolation_stage') - _api.check_in_list(['data', 'rgba'], s=s) + _api.check_in_list(['data', 'rgba', 'auto'], s=s) self._interpolation_stage = s self.stale = True @@ -872,7 +820,7 @@ class AxesImage(_ImageBase): norm : str or `~matplotlib.colors.Normalize` Maps luminance to 0-1. interpolation : str, default: :rc:`image.interpolation` - Supported values are 'none', 'antialiased', 'nearest', 'bilinear', + Supported values are 'none', 'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman'. @@ -910,6 +858,7 @@ def __init__(self, ax, *, cmap=None, norm=None, + colorizer=None, interpolation=None, origin=None, extent=None, @@ -926,6 +875,7 @@ def __init__(self, ax, ax, cmap=cmap, norm=norm, + colorizer=colorizer, interpolation=interpolation, origin=origin, filternorm=filternorm, @@ -948,7 +898,7 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): bbox = Bbox(np.array([[x1, y1], [x2, y2]])) transformed_bbox = TransformedBbox(bbox, trans) clip = ((self.get_clip_box() or self.axes.bbox) if self.get_clip_on() - else self.figure.bbox) + else self.get_figure(root=True).bbox) return self._make_image(self._A, bbox, transformed_bbox, clip, magnification, unsampled=unsampled) @@ -972,10 +922,10 @@ def set_extent(self, extent, **kwargs): Notes ----- - This updates ``ax.dataLim``, and, if autoscaling, sets ``ax.viewLim`` - to tightly fit the image, regardless of ``dataLim``. Autoscaling - state is not changed, so following this with ``ax.autoscale_view()`` - will redo the autoscaling in accord with ``dataLim``. + This updates `.Axes.dataLim`, and, if autoscaling, sets `.Axes.viewLim` + to tightly fit the image, regardless of `~.Axes.dataLim`. Autoscaling + state is not changed, so a subsequent call to `.Axes.autoscale_view` + will redo the autoscaling in accord with `~.Axes.dataLim`. """ (xmin, xmax), (ymin, ymax) = self.axes._process_unit_info( [("x", [extent[0], extent[1]]), @@ -1085,12 +1035,16 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): B[:, :, 0:3] = A B[:, :, 3] = 255 A = B - vl = self.axes.viewLim l, b, r, t = self.axes.bbox.extents width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification) height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification) - x_pix = np.linspace(vl.x0, vl.x1, width) - y_pix = np.linspace(vl.y0, vl.y1, height) + + invertedTransform = self.axes.transData.inverted() + x_pix = invertedTransform.transform( + [(x, b) for x in np.linspace(l, r, width)])[:, 0] + y_pix = invertedTransform.transform( + [(l, y) for y in np.linspace(b, t, height)])[:, 1] + if self._interpolation == "nearest": x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2 y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2 @@ -1178,11 +1132,9 @@ def get_extent(self): raise RuntimeError('Must set data first') return self._Ax[0], self._Ax[-1], self._Ay[0], self._Ay[-1] - @_api.rename_parameter("3.8", "s", "filternorm") def set_filternorm(self, filternorm): pass - @_api.rename_parameter("3.8", "s", "filterrad") def set_filterrad(self, filterrad): pass @@ -1222,6 +1174,7 @@ def __init__(self, ax, *, cmap=None, norm=None, + colorizer=None, **kwargs ): """ @@ -1248,7 +1201,7 @@ def __init__(self, ax, Maps luminance to 0-1. **kwargs : `~matplotlib.artist.Artist` properties """ - super().__init__(ax, norm=norm, cmap=cmap) + super().__init__(ax, norm=norm, cmap=cmap, colorizer=colorizer) self._internal_update(kwargs) if A is not None: self.set_data(x, y, A) @@ -1352,6 +1305,7 @@ def __init__(self, fig, *, cmap=None, norm=None, + colorizer=None, offsetx=0, offsety=0, origin=None, @@ -1367,9 +1321,10 @@ def __init__(self, fig, None, norm=norm, cmap=cmap, + colorizer=colorizer, origin=origin ) - self.figure = fig + self.set_figure(fig) self.ox = offsetx self.oy = offsety self._internal_update(kwargs) @@ -1383,14 +1338,15 @@ def get_extent(self): def make_image(self, renderer, magnification=1.0, unsampled=False): # docstring inherited - fac = renderer.dpi/self.figure.dpi + fig = self.get_figure(root=True) + fac = renderer.dpi/fig.dpi # fac here is to account for pdf, eps, svg backends where # figure.dpi is set to 72. This means we need to scale the # image (using magnification) and offset it appropriately. bbox = Bbox([[self.ox/fac, self.oy/fac], [(self.ox/fac + self._A.shape[1]), (self.oy/fac + self._A.shape[0])]]) - width, height = self.figure.get_size_inches() + width, height = fig.get_size_inches() width *= renderer.dpi height *= renderer.dpi clip = Bbox([[0, 0], [width, height]]) @@ -1400,7 +1356,7 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): def set_data(self, A): """Set the image array.""" - cm.ScalarMappable.set_array(self, A) + super().set_data(A) self.stale = True @@ -1411,6 +1367,7 @@ def __init__(self, bbox, *, cmap=None, norm=None, + colorizer=None, interpolation=None, origin=None, filternorm=True, @@ -1428,6 +1385,7 @@ def __init__(self, bbox, None, cmap=cmap, norm=norm, + colorizer=colorizer, interpolation=interpolation, origin=origin, filternorm=filternorm, @@ -1631,7 +1589,7 @@ def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None, # as is, saving a few operations. rgba = arr else: - sm = cm.ScalarMappable(cmap=cmap) + sm = mcolorizer.Colorizer(cmap=cmap) sm.set_clim(vmin, vmax) rgba = sm.to_rgba(arr, bytes=True) if pil_kwargs is None: @@ -1755,7 +1713,7 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', thus supports a wide range of file formats, including PNG, JPG, TIFF and others. - .. _Pillow: https://python-pillow.org/ + .. _Pillow: https://python-pillow.github.io thumbfile : str or file-like The thumbnail filename. diff --git a/lib/matplotlib/image.pyi b/lib/matplotlib/image.pyi index 4b684f693845..1fcc1a710bfd 100644 --- a/lib/matplotlib/image.pyi +++ b/lib/matplotlib/image.pyi @@ -7,10 +7,10 @@ import numpy as np from numpy.typing import ArrayLike, NDArray import PIL.Image -import matplotlib.artist as martist from matplotlib.axes import Axes -from matplotlib import cm +from matplotlib import colorizer from matplotlib.backend_bases import RendererBase, MouseEvent +from matplotlib.colorizer import Colorizer from matplotlib.colors import Colormap, Normalize from matplotlib.figure import Figure from matplotlib.transforms import Affine2D, BboxBase, Bbox, Transform @@ -58,7 +58,7 @@ def composite_images( images: Sequence[_ImageBase], renderer: RendererBase, magnification: float = ... ) -> tuple[np.ndarray, float, float]: ... -class _ImageBase(martist.Artist, cm.ScalarMappable): +class _ImageBase(colorizer.ColorizingArtist): zorder: float origin: Literal["upper", "lower"] axes: Axes @@ -67,13 +67,14 @@ class _ImageBase(martist.Artist, cm.ScalarMappable): ax: Axes, cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., + colorizer: Colorizer | None = ..., interpolation: str | None = ..., origin: Literal["upper", "lower"] | None = ..., filternorm: bool = ..., filterrad: float = ..., resample: bool | None = ..., *, - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., **kwargs ) -> None: ... def get_size(self) -> tuple[int, int]: ... @@ -89,8 +90,8 @@ class _ImageBase(martist.Artist, cm.ScalarMappable): def get_shape(self) -> tuple[int, int, int]: ... def get_interpolation(self) -> str: ... def set_interpolation(self, s: str | None) -> None: ... - def get_interpolation_stage(self) -> Literal["data", "rgba"]: ... - def set_interpolation_stage(self, s: Literal["data", "rgba"]) -> None: ... + def get_interpolation_stage(self) -> Literal["data", "rgba", "auto"]: ... + def set_interpolation_stage(self, s: Literal["data", "rgba", "auto"]) -> None: ... def can_composite(self) -> bool: ... def set_resample(self, v: bool | None) -> None: ... def get_resample(self) -> bool: ... @@ -106,13 +107,14 @@ class AxesImage(_ImageBase): *, cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., + colorizer: Colorizer | None = ..., interpolation: str | None = ..., origin: Literal["upper", "lower"] | None = ..., extent: tuple[float, float, float, float] | None = ..., filternorm: bool = ..., filterrad: float = ..., resample: bool = ..., - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., **kwargs ) -> None: ... def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ... @@ -144,6 +146,7 @@ class PcolorImage(AxesImage): *, cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., + colorizer: Colorizer | None = ..., **kwargs ) -> None: ... def set_data(self, x: ArrayLike, y: ArrayLike, A: ArrayLike) -> None: ... # type: ignore[override] @@ -160,6 +163,7 @@ class FigureImage(_ImageBase): *, cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., + colorizer: Colorizer | None = ..., offsetx: int = ..., offsety: int = ..., origin: Literal["upper", "lower"] | None = ..., @@ -175,6 +179,7 @@ class BboxImage(_ImageBase): *, cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., + colorizer: Colorizer | None = ..., interpolation: str | None = ..., origin: Literal["upper", "lower"] | None = ..., filternorm: bool = ..., diff --git a/lib/matplotlib/inset.py b/lib/matplotlib/inset.py new file mode 100644 index 000000000000..bab69491303e --- /dev/null +++ b/lib/matplotlib/inset.py @@ -0,0 +1,269 @@ +""" +The inset module defines the InsetIndicator class, which draws the rectangle and +connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`. +""" + +from . import _api, artist, transforms +from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle +from matplotlib.path import Path + + +_shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth') + + +class InsetIndicator(artist.Artist): + """ + An artist to highlight an area of interest. + + An inset indicator is a rectangle on the plot at the position indicated by + *bounds* that optionally has lines that connect the rectangle to an inset + Axes (`.Axes.inset_axes`). + + .. versionadded:: 3.10 + """ + zorder = 4.99 + + def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs): + """ + Parameters + ---------- + bounds : [x0, y0, width, height], optional + Lower-left corner of rectangle to be marked, and its width + and height. If not set, the bounds will be calculated from the + data limits of inset_ax, which must be supplied. + + inset_ax : `~.axes.Axes`, optional + An optional inset Axes to draw connecting lines to. Two lines are + drawn connecting the indicator box to the inset Axes on corners + chosen so as to not overlap with the indicator box. + + zorder : float, default: 4.99 + Drawing order of the rectangle and connector lines. The default, + 4.99, is just below the default level of inset Axes. + + **kwargs + Other keyword arguments are passed on to the `.Rectangle` patch. + """ + if bounds is None and inset_ax is None: + raise ValueError("At least one of bounds or inset_ax must be supplied") + + self._inset_ax = inset_ax + + if bounds is None: + # Work out bounds from inset_ax + self._auto_update_bounds = True + bounds = self._bounds_from_inset_ax() + else: + self._auto_update_bounds = False + + x, y, width, height = bounds + + self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs) + + # Connector positions cannot be calculated till the artist has been added + # to an axes, so just make an empty list for now. + self._connectors = [] + + super().__init__() + self.set_zorder(zorder) + + # Initial style properties for the artist should match the rectangle. + for prop in _shared_properties: + setattr(self, f'_{prop}', artist.getp(self._rectangle, prop)) + + def _shared_setter(self, prop, val): + """ + Helper function to set the same style property on the artist and its children. + """ + setattr(self, f'_{prop}', val) + + artist.setp([self._rectangle, *self._connectors], prop, val) + + def set_alpha(self, alpha): + # docstring inherited + self._shared_setter('alpha', alpha) + + def set_edgecolor(self, color): + """ + Set the edge color of the rectangle and the connectors. + + Parameters + ---------- + color : :mpltype:`color` or None + """ + self._shared_setter('edgecolor', color) + + def set_color(self, c): + """ + Set the edgecolor of the rectangle and the connectors, and the + facecolor for the rectangle. + + Parameters + ---------- + c : :mpltype:`color` + """ + self._shared_setter('edgecolor', c) + self._shared_setter('facecolor', c) + + def set_linewidth(self, w): + """ + Set the linewidth in points of the rectangle and the connectors. + + Parameters + ---------- + w : float or None + """ + self._shared_setter('linewidth', w) + + 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 + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= + + Alternatively a dash tuple of the following form can be provided:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple of on and off ink in points. + + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + The line style. + """ + self._shared_setter('linestyle', ls) + + def _bounds_from_inset_ax(self): + xlim = self._inset_ax.get_xlim() + ylim = self._inset_ax.get_ylim() + return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) + + def _update_connectors(self): + (x, y) = self._rectangle.get_xy() + width = self._rectangle.get_width() + height = self._rectangle.get_height() + + existing_connectors = self._connectors or [None] * 4 + + # connect the inset_axes to the rectangle + for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)], + existing_connectors): + # inset_ax positions are in axes coordinates + # The 0, 1 values define the four edges if the inset_ax + # lower_left, upper_left, lower_right upper_right. + ex, ey = xy_inset_ax + if self.axes.xaxis.get_inverted(): + ex = 1 - ex + if self.axes.yaxis.get_inverted(): + ey = 1 - ey + xy_data = x + ex * width, y + ey * height + if existing is None: + # Create new connection patch with styles inherited from the + # parent artist. + p = ConnectionPatch( + xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes, + xyB=xy_data, coordsB=self.axes.transData, + arrowstyle="-", + edgecolor=self._edgecolor, alpha=self.get_alpha(), + linestyle=self._linestyle, linewidth=self._linewidth) + self._connectors.append(p) + else: + # Only update positioning of existing connection patch. We + # do not want to override any style settings made by the user. + existing.xy1 = xy_inset_ax + existing.xy2 = xy_data + existing.coords1 = self._inset_ax.transAxes + existing.coords2 = self.axes.transData + + if existing is None: + # decide which two of the lines to keep visible.... + pos = self._inset_ax.get_position() + bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) + rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed( + self._rectangle.get_transform()) + x0 = rectbbox.x0 < bboxins.x0 + x1 = rectbbox.x1 < bboxins.x1 + y0 = rectbbox.y0 < bboxins.y0 + y1 = rectbbox.y1 < bboxins.y1 + self._connectors[0].set_visible(x0 ^ y0) + self._connectors[1].set_visible(x0 == y1) + self._connectors[2].set_visible(x1 == y0) + self._connectors[3].set_visible(x1 ^ y1) + + @property + def rectangle(self): + """`.Rectangle`: the indicator frame.""" + return self._rectangle + + @property + def connectors(self): + """ + 4-tuple of `.patches.ConnectionPatch` or None + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + """ + if self._inset_ax is None: + return + + if self._auto_update_bounds: + self._rectangle.set_bounds(self._bounds_from_inset_ax()) + self._update_connectors() + return tuple(self._connectors) + + def draw(self, renderer): + # docstring inherited + conn_same_style = [] + + # Figure out which connectors have the same style as the box, so should + # be drawn as a single path. + for conn in self.connectors or []: + if conn.get_visible(): + drawn = False + for s in _shared_properties: + if artist.getp(self._rectangle, s) != artist.getp(conn, s): + # Draw this connector by itself + conn.draw(renderer) + drawn = True + break + + if not drawn: + # Connector has same style as box. + conn_same_style.append(conn) + + if conn_same_style: + # Since at least one connector has the same style as the rectangle, draw + # them as a compound path. + artists = [self._rectangle] + conn_same_style + paths = [a.get_transform().transform_path(a.get_path()) for a in artists] + path = Path.make_compound_path(*paths) + + # Create a temporary patch to draw the path. + p = PathPatch(path) + p.update_from(self._rectangle) + p.set_transform(transforms.IdentityTransform()) + p.draw(renderer) + + return + + # Just draw the rectangle + self._rectangle.draw(renderer) + + @_api.deprecated( + '3.10', + message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single ' + 'InsetIndicator artist with a rectangle property and a connectors ' + 'property. From 3.12 it will no longer be possible to unpack the ' + 'return value into two elements.')) + def __getitem__(self, key): + return [self._rectangle, self.connectors][key] diff --git a/lib/matplotlib/inset.pyi b/lib/matplotlib/inset.pyi new file mode 100644 index 000000000000..e895fd7be27c --- /dev/null +++ b/lib/matplotlib/inset.pyi @@ -0,0 +1,25 @@ +from . import artist +from .axes import Axes +from .backend_bases import RendererBase +from .patches import ConnectionPatch, Rectangle + +from .typing import ColorType, LineStyleType + +class InsetIndicator(artist.Artist): + def __init__( + self, + bounds: tuple[float, float, float, float] | None = ..., + inset_ax: Axes | None = ..., + zorder: float | None = ..., + **kwargs + ) -> None: ... + def set_alpha(self, alpha: float | None) -> None: ... + def set_edgecolor(self, color: ColorType | None) -> None: ... + def set_color(self, c: ColorType | None) -> None: ... + def set_linewidth(self, w: float | None) -> None: ... + def set_linestyle(self, ls: LineStyleType | None) -> None: ... + @property + def rectangle(self) -> Rectangle: ... + @property + def connectors(self) -> tuple[ConnectionPatch, ConnectionPatch, ConnectionPatch, ConnectionPatch] | None: ... + def draw(self, renderer: RendererBase) -> None: ... diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 9033fc23c1a1..ace3f668e740 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -98,8 +98,8 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _legend_kw_doc_base = """ bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. - Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or - `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary + Defaults to ``axes.bbox`` (if called as a method to `.Axes.legend`) or + ``figure.bbox`` (if ``figure.legend``). This argument allows arbitrary placement of the legend. Bbox coordinates are interpreted in the coordinate system given by @@ -305,7 +305,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', best=_loc_doc_best, outside='') + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) +_docstring.interpd.register(_legend_kw_axes=_legend_kw_axes_st) _outside_doc = """ If a figure is using the constrained layout manager, the string codes @@ -323,20 +323,20 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _loc_doc_base.format(parent='figure', default="'upper right'", best='', outside=_outside_doc) + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) +_docstring.interpd.register(_legend_kw_figure=_legend_kw_figure_st) _legend_kw_both_st = ( _loc_doc_base.format(parent='axes/figure', default=":rc:`legend.loc` for Axes, 'upper right' for Figure", best=_loc_doc_best, outside=_outside_doc) + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) +_docstring.interpd.register(_legend_kw_doc=_legend_kw_both_st) _legend_kw_set_loc_st = ( _loc_doc_base.format(parent='axes/figure', default=":rc:`legend.loc` for Axes, 'upper right' for Figure", best=_loc_doc_best, outside=_outside_doc)) -_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) +_docstring.interpd.register(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) class Legend(Artist): @@ -351,7 +351,7 @@ class Legend(Artist): def __str__(self): return "Legend" - @_docstring.dedent_interpd + @_docstring.interpd def __init__( self, parent, handles, labels, *, @@ -454,24 +454,10 @@ def __init__( self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad') self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing') self.shadow = mpl._val_or_rc(shadow, 'legend.shadow') - # trim handles and labels if illegal label... - _lab, _hand = [], [] - for label, handle in zip(labels, handles): - if isinstance(label, str) and label.startswith('_'): - _api.warn_deprecated("3.8", message=( - "An artist whose label starts with an underscore was passed to " - "legend(); such artists will no longer be ignored in the future. " - "To suppress this warning, explicitly filter out such artists, " - "e.g. with `[art for art in artists if not " - "art.get_label().startswith('_')]`.")) - else: - _lab.append(label) - _hand.append(handle) - labels, handles = _lab, _hand if reverse: - labels.reverse() - handles.reverse() + labels = [*reversed(labels)] + handles = [*reversed(handles)] if len(handles) < 2: ncols = 1 @@ -497,7 +483,7 @@ def __init__( if isinstance(parent, Axes): self.isaxes = True self.axes = parent - self.set_figure(parent.figure) + self.set_figure(parent.get_figure(root=False)) elif isinstance(parent, FigureBase): self.isaxes = False self.set_figure(parent) @@ -637,13 +623,13 @@ def _set_artist_props(self, a): """ Set the boilerplate props for artists added to Axes. """ - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) if self.isaxes: a.axes = self.axes a.set_transform(self.get_transform()) - @_docstring.dedent_interpd + @_docstring.interpd def set_loc(self, loc=None): """ Set the location of the legend. @@ -943,7 +929,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): align=self._alignment, children=[self._legend_title_box, self._legend_handle_box]) - self._legend_box.set_figure(self.figure) + self._legend_box.set_figure(self.get_figure(root=False)) self._legend_box.axes = self.axes self.texts = text_list self.legend_handles = handle_list @@ -1065,7 +1051,7 @@ def get_title(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() return self._legend_box.get_window_extent(renderer=renderer) def get_tightbbox(self, renderer=None): @@ -1196,7 +1182,6 @@ def _find_best_position(self, width, height, renderer): return l, b - @_api.rename_parameter("3.8", "event", "mouseevent") def contains(self, mouseevent): return self.legendPatch.contains(mouseevent) @@ -1338,7 +1323,7 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): _api.warn_deprecated("3.9", message=( "You have mixed positional and keyword arguments, some input may " "be discarded. This is deprecated since %(since)s and will " - "become an error %(removal)s.")) + "become an error in %(removal)s.")) if (hasattr(handles, "__len__") and hasattr(labels, "__len__") and diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 5a929070e32d..97076ad09cb8 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -466,7 +466,7 @@ def update_prop(self, legend_handle, orig_handle, legend): self._update_prop(legend_handle, orig_handle) - legend_handle.set_figure(legend.figure) + legend_handle.set_figure(legend.get_figure(root=False)) # legend._set_artist_props(legend_handle) legend_handle.set_clip_box(None) legend_handle.set_clip_path(None) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index d24e528e4c0a..65a4ccb6d950 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -467,11 +467,12 @@ def contains(self, mouseevent): yt = xy[:, 1] # Convert pick radius from points to pixels - if self.figure is None: + fig = self.get_figure(root=True) + if fig is None: _log.warning('no figure set when check if mouse is on line') pixels = self._pickradius else: - pixels = self.figure.dpi / 72. * self._pickradius + pixels = fig.dpi / 72. * self._pickradius # The math involved in checking for containment (here and inside of # segment_hits) assumes that it is OK to overflow, so temporarily set @@ -640,7 +641,7 @@ def get_window_extent(self, renderer=None): ignore=True) # correct for marker size, if any if self._marker: - ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5 + ms = (self._markersize / 72.0 * self.get_figure(root=True).dpi) * 0.5 bbox = bbox.padded(ms) return bbox @@ -1541,45 +1542,65 @@ def draw(self, renderer): super().draw(renderer) def get_xy1(self): - """ - Return the *xy1* value of the line. - """ + """Return the *xy1* value of the line.""" return self._xy1 def get_xy2(self): - """ - Return the *xy2* value of the line. - """ + """Return the *xy2* value of the line.""" return self._xy2 def get_slope(self): - """ - Return the *slope* value of the line. - """ + """Return the *slope* value of the line.""" return self._slope - def set_xy1(self, x, y): + def set_xy1(self, *args, **kwargs): """ Set the *xy1* value of the line. Parameters ---------- - x, y : float + xy1 : tuple[float, float] Points for the line to pass through. """ - self._xy1 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy1: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy1 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy1 = params["x"], params["y"] + else: + xy1 = params["xy1"] + self._xy1 = xy1 - def set_xy2(self, x, y): + def set_xy2(self, *args, **kwargs): """ Set the *xy2* value of the line. + .. note:: + + You can only set *xy2* if the line was created using the *xy2* + parameter. If the line was created using *slope*, please use + `~.AxLine.set_slope`. + Parameters ---------- - x, y : float + xy2 : tuple[float, float] Points for the line to pass through. """ if self._slope is None: - self._xy2 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy2: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy2 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy2 = params["x"], params["y"] + else: + xy2 = params["xy2"] + self._xy2 = xy2 else: raise ValueError("Cannot set an 'xy2' value while 'slope' is set;" " they differ but their functionalities overlap") @@ -1588,6 +1609,12 @@ def set_slope(self, slope): """ Set the *slope* value of the line. + .. note:: + + You can only set *slope* if the line was created using the *slope* + parameter. If the line was created using *xy2*, please use + `~.AxLine.set_xy2`. + Parameters ---------- slope : float @@ -1648,7 +1675,7 @@ def __init__(self, line): 'pick_event', self.onpick) self.ind = set() - canvas = property(lambda self: self.axes.figure.canvas) + canvas = property(lambda self: self.axes.get_figure(root=True).canvas) def process_selected(self, ind, xs, ys): """ diff --git a/lib/matplotlib/lines.pyi b/lib/matplotlib/lines.pyi index c91e457e3301..7989a03dae3a 100644 --- a/lib/matplotlib/lines.pyi +++ b/lib/matplotlib/lines.pyi @@ -2,7 +2,7 @@ from .artist import Artist from .axes import Axes from .backend_bases import MouseEvent, FigureCanvasBase from .path import Path -from .transforms import Bbox, Transform +from .transforms import Bbox from collections.abc import Callable, Sequence from typing import Any, Literal, overload @@ -130,8 +130,8 @@ class AxLine(Line2D): def get_xy1(self) -> tuple[float, float] | None: ... def get_xy2(self) -> tuple[float, float] | None: ... def get_slope(self) -> float: ... - def set_xy1(self, x: float, y: float) -> None: ... - def set_xy2(self, x: float, y: float) -> None: ... + def set_xy1(self, xy1: tuple[float, float]) -> None: ... + def set_xy2(self, xy2: tuple[float, float]) -> None: ... def set_slope(self, slope: float) -> None: ... class VertexSelector: diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index e25b76c4037c..a88c35c15676 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -20,9 +20,9 @@ import matplotlib as mpl from matplotlib import _api, _mathtext -from matplotlib.ft2font import LOAD_NO_HINTING +from matplotlib.ft2font import LoadFlags from matplotlib.font_manager import FontProperties -from ._mathtext import ( # noqa: reexported API +from ._mathtext import ( # noqa: F401, reexported API RasterParse, VectorParse, get_unicode_index) _log = logging.getLogger(__name__) @@ -71,27 +71,27 @@ def parse(self, s, dpi=72, prop=None, *, antialiased=None): Depending on the *output* type, this returns either a `VectorParse` or a `RasterParse`. """ - # lru_cache can't decorate parse() directly because prop - # is mutable; key the cache using an internal copy (see - # text._get_text_metrics_with_cache for a similar case). + # lru_cache can't decorate parse() directly because prop is + # mutable, so we key the cache using an internal copy (see + # Text._get_text_metrics_with_cache for a similar case); likewise, + # we need to check the mutable state of the text.antialiased and + # text.hinting rcParams. prop = prop.copy() if prop is not None else None antialiased = mpl._val_or_rc(antialiased, 'text.antialiased') - return self._parse_cached(s, dpi, prop, antialiased) - - @functools.lru_cache(50) - def _parse_cached(self, s, dpi, prop, antialiased): from matplotlib.backends import backend_agg + load_glyph_flags = { + "vector": LoadFlags.NO_HINTING, + "raster": backend_agg.get_hinting_flag(), + }[self._output_type] + return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags) + @functools.lru_cache(50) + def _parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags): if prop is None: prop = FontProperties() fontset_class = _api.check_getitem( self._font_type_mapping, fontset=prop.get_math_fontfamily()) - load_glyph_flags = { - "vector": LOAD_NO_HINTING, - "raster": backend_agg.get_hinting_flag(), - }[self._output_type] fontset = fontset_class(prop, load_glyph_flags) - fontsize = prop.get_size_in_points() if self._parser is None: # Cache the parser globally. diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index c4b66fc1b336..c4746f332bcb 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -4,7 +4,9 @@ python_sources = [ '_animation_data.py', '_blocking_input.py', '_cm.py', + '_cm_bivar.py', '_cm_listed.py', + '_cm_multivar.py', '_color_data.py', '_constrained_layout.py', '_docstring.py', @@ -31,6 +33,7 @@ python_sources = [ 'cm.py', 'collections.py', 'colorbar.py', + 'colorizer.py', 'colors.py', 'container.py', 'contour.py', @@ -41,6 +44,7 @@ python_sources = [ 'gridspec.py', 'hatch.py', 'image.py', + 'inset.py', 'layout_engine.py', 'legend_handler.py', 'legend.py', @@ -87,7 +91,6 @@ typing_sources = [ '_enums.pyi', '_path.pyi', '_pylab_helpers.pyi', - '_ttconv.pyi', 'animation.pyi', 'artist.pyi', 'axis.pyi', @@ -99,6 +102,7 @@ typing_sources = [ 'cm.pyi', 'collections.pyi', 'colorbar.pyi', + 'colorizer.pyi', 'colors.pyi', 'container.pyi', 'contour.pyi', @@ -108,6 +112,7 @@ typing_sources = [ 'gridspec.pyi', 'hatch.pyi', 'image.pyi', + 'inset.pyi', 'layout_engine.pyi', 'legend_handler.pyi', 'legend.pyi', diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index e1f08c0da5ce..8326ac186e31 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -400,7 +400,7 @@ def _single_spectrum_helper( # Split out these keyword docs so that they can be used elsewhere -_docstring.interpd.update( +_docstring.interpd.register( Spectral="""\ Fs : float, default: 2 The sampling frequency (samples per time unit). It is used to calculate @@ -458,7 +458,7 @@ def _single_spectrum_helper( MATLAB compatibility.""") -@_docstring.dedent_interpd +@_docstring.interpd def psd(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): r""" @@ -514,7 +514,7 @@ def psd(x, NFFT=None, Fs=None, detrend=None, window=None, return Pxx.real, freqs -@_docstring.dedent_interpd +@_docstring.interpd def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): """ @@ -634,7 +634,7 @@ def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, **_docstring.interpd.params) -@_docstring.dedent_interpd +@_docstring.interpd def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, mode=None): @@ -717,7 +717,7 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, return spec, freqs, t -@_docstring.dedent_interpd +@_docstring.interpd def cohere(x, y, NFFT=256, Fs=2, detrend=detrend_none, window=window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None): r""" diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 29ffb20f4280..df4f4afadf96 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -148,8 +148,14 @@ ## for more information on patch properties. #patch.linewidth: 1.0 # edge width in points. #patch.facecolor: C0 -#patch.edgecolor: black # if forced, or patch is not filled -#patch.force_edgecolor: False # True to always use 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. +#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. #patch.antialiased: True # render patches in antialiased (no jaggies) @@ -433,6 +439,11 @@ #axes3d.yaxis.panecolor: (0.90, 0.90, 0.90, 0.5) # background pane on 3D axes #axes3d.zaxis.panecolor: (0.925, 0.925, 0.925, 0.5) # background pane on 3D axes +#axes3d.mouserotationstyle: arcball # {azel, trackball, sphere, arcball} + # See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse +#axes3d.trackballsize: 0.667 # trackball diameter, in units of the Axes bbox +#axes3d.trackballborder: 0.2 # trackball border width, in units of the Axes bbox (only for 'sphere' and 'arcball' style) + ## *************************************************************************** ## * AXIS * ## *************************************************************************** @@ -602,8 +613,8 @@ ## * IMAGES * ## *************************************************************************** #image.aspect: equal # {equal, auto} or a number -#image.interpolation: antialiased # see help(imshow) for options -#image.interpolation_stage: data # see help(imshow) for options +#image.interpolation: auto # see help(imshow) for options +#image.interpolation_stage: auto # see help(imshow) for options #image.cmap: viridis # A colormap name (plasma, magma, etc.) #image.lut: 256 # the size of the colormap lookup table #image.origin: upper # {lower, upper} @@ -671,7 +682,7 @@ # to the nearest pixel when certain criteria are met. # When False, paths will never be snapped. #path.sketch: None # May be None, or a tuple of the form: - # path.sketch: (scale, length, randomness) + # 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 @@ -735,6 +746,8 @@ # None: Assume fonts are installed on the # machine where the SVG will be viewed. #svg.hashsalt: None # If not None, use this string as hash salt instead of uuid4 +#svg.id: None # If not None, use this string as the value for the `id` + # attribute in the top tag ### pgf parameter ## See https://matplotlib.org/stable/tutorials/text/pgf.html for more information. diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index 976ab291907b..50516d831ae4 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -380,7 +380,6 @@ boxplot.showbox: True boxplot.showcaps: True boxplot.showfliers: True boxplot.showmeans: False -boxplot.vertical: True boxplot.whiskerprops.color: b boxplot.whiskerprops.linestyle: -- boxplot.whiskerprops.linewidth: 1.0 diff --git a/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle b/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle index c4b7741ae440..61a99f3c0d10 100644 --- a/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle @@ -18,9 +18,6 @@ grid.color: white figure.facecolor: black figure.edgecolor: black -savefig.facecolor: black -savefig.edgecolor: black - ### Boxplots boxplot.boxprops.color: white boxplot.capprops.color: white diff --git a/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle b/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle index 738db39f5f80..cd56d404c3b5 100644 --- a/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle @@ -29,10 +29,7 @@ xtick.minor.size: 0 ytick.major.size: 0 ytick.minor.size: 0 -font.size:14.0 - -savefig.edgecolor: f0f0f0 -savefig.facecolor: f0f0f0 +font.size: 14.0 figure.subplot.left: 0.08 figure.subplot.right: 0.95 diff --git a/lib/matplotlib/mpl-data/stylelib/petroff10.mplstyle b/lib/matplotlib/mpl-data/stylelib/petroff10.mplstyle new file mode 100644 index 000000000000..62d1262a09cd --- /dev/null +++ b/lib/matplotlib/mpl-data/stylelib/petroff10.mplstyle @@ -0,0 +1,5 @@ +# Color cycle survey palette from Petroff (2021): +# https://arxiv.org/abs/2107.02270 +# https://github.com/mpetroff/accessible-color-cycles +axes.prop_cycle: cycler('color', ['3f90da', 'ffa90e', 'bd1f01', '94a4a2', '832db6', 'a96b59', 'e76300', 'b9ac70', '717581', '92dadd']) +patch.facecolor: 3f90da diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 194b950a8a30..49f0946f1ee9 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -363,7 +363,7 @@ def get_bbox(self, renderer): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() bbox = self.get_bbox(renderer) try: # Some subclasses redefine get_offset to take no args. px, py = self.get_offset(bbox, renderer) @@ -644,7 +644,7 @@ def add_artist(self, a): a.set_transform(self.get_transform()) if self.axes is not None: a.axes = self.axes - fig = self.figure + fig = self.get_figure(root=False) if fig is not None: a.set_figure(fig) @@ -1191,7 +1191,7 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): def __str__(self): return f"AnnotationBbox({self.xy[0]:g},{self.xy[1]:g})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, offsetbox, xy, xybox=None, xycoords='data', boxcoords=None, *, frameon=True, pad=0.4, # FancyBboxPatch boxstyle. annotation_clip=None, @@ -1356,7 +1356,7 @@ def get_fontsize(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_window_extent(renderer) for child in self.get_children()]) @@ -1364,7 +1364,7 @@ def get_window_extent(self, renderer=None): def get_tightbbox(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_tightbbox(renderer) for child in self.get_children()]) @@ -1412,8 +1412,9 @@ def draw(self, renderer): renderer.open_group(self.__class__.__name__, gid=self.get_gid()) self.update_positions(renderer) if self.arrow_patch is not None: - if self.arrow_patch.figure is None and self.figure is not None: - self.arrow_patch.figure = self.figure + if (self.arrow_patch.get_figure(root=False) is None and + (fig := self.get_figure(root=False)) is not None): + self.arrow_patch.set_figure(fig) self.arrow_patch.draw(renderer) self.patch.draw(renderer) self.offsetbox.draw(renderer) @@ -1468,7 +1469,7 @@ def __init__(self, ref_artist, use_blit=False): ] # A property, not an attribute, to maintain picklability. - canvas = property(lambda self: self.ref_artist.figure.canvas) + canvas = property(lambda self: self.ref_artist.get_figure(root=True).canvas) cids = property(lambda self: [ disconnect.args[0] for disconnect in self._disconnectors[:2]]) @@ -1480,7 +1481,7 @@ def on_motion(self, evt): if self._use_blit: self.canvas.restore_region(self.background) self.ref_artist.draw( - self.ref_artist.figure._get_renderer()) + self.ref_artist.get_figure(root=True)._get_renderer()) self.canvas.blit() else: self.canvas.draw() @@ -1495,10 +1496,9 @@ def on_pick(self, evt): if self.got_artist and self._use_blit: self.ref_artist.set_animated(True) self.canvas.draw() - self.background = \ - self.canvas.copy_from_bbox(self.ref_artist.figure.bbox) - self.ref_artist.draw( - self.ref_artist.figure._get_renderer()) + fig = self.ref_artist.get_figure(root=False) + self.background = self.canvas.copy_from_bbox(fig.bbox) + self.ref_artist.draw(fig._get_renderer()) self.canvas.blit() def on_release(self, event): @@ -1512,7 +1512,7 @@ def on_release(self, event): self.ref_artist.set_animated(False) def _check_still_parented(self): - if self.ref_artist.figure is None: + if self.ref_artist.get_figure(root=False) is None: self.disconnect() return False else: @@ -1540,7 +1540,7 @@ def __init__(self, ref_artist, offsetbox, use_blit=False): def save_offset(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._get_renderer() + renderer = offsetbox.get_figure(root=True)._get_renderer() offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer) self.offsetbox_x, self.offsetbox_y = offset self.offsetbox.set_offset(offset) @@ -1551,7 +1551,7 @@ def update_offset(self, dx, dy): def get_loc_in_canvas(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._get_renderer() + renderer = offsetbox.get_figure(root=True)._get_renderer() bbox = offsetbox.get_bbox(renderer) ox, oy = offsetbox._offset loc_in_canvas = (ox + bbox.x0, oy + bbox.y0) diff --git a/lib/matplotlib/offsetbox.pyi b/lib/matplotlib/offsetbox.pyi index c222a9b2973e..3b1520e17138 100644 --- a/lib/matplotlib/offsetbox.pyi +++ b/lib/matplotlib/offsetbox.pyi @@ -2,11 +2,12 @@ import matplotlib.artist as martist from matplotlib.backend_bases import RendererBase, Event, FigureCanvasBase from matplotlib.colors import Colormap, Normalize import matplotlib.text as mtext -from matplotlib.figure import Figure +from matplotlib.figure import Figure, SubFigure from matplotlib.font_manager import FontProperties from matplotlib.image import BboxImage from matplotlib.patches import FancyArrowPatch, FancyBboxPatch from matplotlib.transforms import Bbox, BboxBase, Transform +from matplotlib.typing import CoordsType import numpy as np from numpy.typing import ArrayLike @@ -26,7 +27,7 @@ class OffsetBox(martist.Artist): width: float | None height: float | None def __init__(self, *args, **kwargs) -> None: ... - def set_figure(self, fig: Figure) -> None: ... + def set_figure(self, fig: Figure | SubFigure) -> None: ... def set_offset( self, xy: tuple[float, float] @@ -219,9 +220,7 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): offsetbox: OffsetBox arrowprops: dict[str, Any] | None xybox: tuple[float, float] - boxcoords: str | tuple[str, str] | martist.Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ] + boxcoords: CoordsType arrow_patch: FancyArrowPatch | None patch: FancyBboxPatch prop: FontProperties @@ -230,17 +229,8 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): offsetbox: OffsetBox, xy: tuple[float, float], xybox: tuple[float, float] | None = ..., - xycoords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., - boxcoords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | None = ..., + xycoords: CoordsType = ..., + boxcoords: CoordsType | None = ..., *, frameon: bool = ..., pad: float = ..., @@ -258,20 +248,14 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): @property def anncoords( self, - ) -> str | tuple[str, str] | martist.Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @anncoords.setter def anncoords( self, - coords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... def get_children(self) -> list[martist.Artist]: ... - def set_figure(self, fig: Figure) -> None: ... + def set_figure(self, fig: Figure | SubFigure) -> None: ... def set_fontsize(self, s: str | float | None = ...) -> None: ... def get_fontsize(self) -> float: ... def get_tightbbox(self, renderer: RendererBase | None = ...) -> Bbox: ... diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 2899952634a9..f47c8abee32d 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -72,6 +72,7 @@ def __init__(self, *, joinstyle = JoinStyle.miter self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color']) + self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self._fill = bool(fill) # needed for set_facecolor call if color is not None: if edgecolor is not None or facecolor is not None: @@ -571,6 +572,14 @@ def get_hatch(self): """Return the hatching pattern.""" return self._hatch + def set_hatch_linewidth(self, lw): + """Set the hatch linewidth.""" + self._hatch_linewidth = lw + + def get_hatch_linewidth(self): + """Return the hatch linewidth.""" + return self._hatch_linewidth + def _draw_paths_with_artist_properties( self, renderer, draw_path_args_list): """ @@ -605,6 +614,7 @@ def _draw_paths_with_artist_properties( if self._hatch: gc.set_hatch(self._hatch) gc.set_hatch_color(self._hatch_color) + gc.set_hatch_linewidth(self._hatch_linewidth) if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) @@ -655,7 +665,7 @@ class Shadow(Patch): def __str__(self): return f"Shadow({self.patch})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, patch, ox, oy, *, shade=0.7, **kwargs): """ Create a shadow of the given *patch*. @@ -735,7 +745,7 @@ def __str__(self): fmt = "Rectangle(xy=(%g, %g), width=%g, height=%g, angle=%g)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0.0, rotation_point='xy', **kwargs): """ @@ -936,7 +946,7 @@ def __str__(self): return s % (self.xy[0], self.xy[1], self.numvertices, self.radius, self.orientation) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, numVertices, *, radius=5, orientation=0, **kwargs): """ @@ -986,7 +996,7 @@ def __str__(self): s = "PathPatch%d((%g, %g) ...)" return s % (len(self._path.vertices), *tuple(self._path.vertices[0])) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, path, **kwargs): """ *path* is a `.Path` object. @@ -1015,7 +1025,7 @@ class StepPatch(PathPatch): _edge_default = False - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, values, edges, *, orientation='vertical', baseline=0, **kwargs): """ @@ -1124,7 +1134,7 @@ def __str__(self): else: return "Polygon0()" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, *, closed=True, **kwargs): """ Parameters @@ -1222,7 +1232,7 @@ def __str__(self): fmt = "Wedge(center=(%g, %g), r=%g, theta1=%g, theta2=%g, width=%s)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, center, r, theta1, theta2, *, width=None, **kwargs): """ A wedge centered at *x*, *y* center with radius *r* that @@ -1310,7 +1320,7 @@ def __str__(self): [0.0, 0.1], [0.0, -0.1], [0.8, -0.1], [0.8, -0.3], [1.0, 0.0], [0.8, 0.3], [0.8, 0.1]]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, x, y, dx, dy, *, width=1.0, **kwargs): """ Draws an arrow from (*x*, *y*) to (*x* + *dx*, *y* + *dy*). @@ -1393,7 +1403,7 @@ class FancyArrow(Polygon): def __str__(self): return "FancyArrow()" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, x, y, dx, dy, *, width=0.001, length_includes_head=False, head_width=None, head_length=None, shape='full', overhang=0, @@ -1552,7 +1562,7 @@ def _make_verts(self): ] -_docstring.interpd.update( +_docstring.interpd.register( FancyArrow="\n".join( (inspect.getdoc(FancyArrow.__init__) or "").splitlines()[2:])) @@ -1564,7 +1574,7 @@ def __str__(self): s = "CirclePolygon((%g, %g), radius=%g, resolution=%d)" return s % (self.xy[0], self.xy[1], self.radius, self.numvertices) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, radius=5, *, resolution=20, # the number of vertices ** kwargs): @@ -1591,7 +1601,7 @@ def __str__(self): fmt = "Ellipse(xy=(%s, %s), width=%s, height=%s, angle=%s)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0, **kwargs): """ Parameters @@ -1767,7 +1777,7 @@ class Annulus(Patch): An elliptical annulus. """ - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, r, width, angle=0.0, **kwargs): """ Parameters @@ -1958,7 +1968,7 @@ def __str__(self): fmt = "Circle(xy=(%g, %g), radius=%g)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, radius=5, **kwargs): """ Create a true circle at center *xy* = (*x*, *y*) with given *radius*. @@ -2005,7 +2015,7 @@ def __str__(self): "height=%g, angle=%g, theta1=%g, theta2=%g)") return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0.0, theta1=0.0, theta2=360.0, **kwargs): """ @@ -2161,7 +2171,7 @@ def segment_circle_intersect(x0, y0, x1, y1): # the unit circle in the same way that it is relative to the desired # ellipse. box_path_transform = ( - transforms.BboxTransformTo((self.axes or self.figure).bbox) + transforms.BboxTransformTo((self.axes or self.get_figure(root=False)).bbox) - self.get_transform()) box_path = Path.unit_rectangle().transformed(box_path_transform) @@ -2290,7 +2300,7 @@ def __init_subclass__(cls): # - %(BoxStyle:table_and_accepts)s # - %(ConnectionStyle:table_and_accepts)s # - %(ArrowStyle:table_and_accepts)s - _docstring.interpd.update({ + _docstring.interpd.register(**{ f"{cls.__name__}:table": cls.pprint_styles(), f"{cls.__name__}:table_and_accepts": ( cls.pprint_styles() @@ -2347,6 +2357,11 @@ def pprint_styles(cls): return textwrap.indent(rst_table, prefix=' ' * 4) @classmethod + @_api.deprecated( + '3.10.0', + message="This method is never used internally.", + 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): @@ -2362,7 +2377,7 @@ def _register_style(style_list, cls=None, *, name=None): return cls -@_docstring.dedent_interpd +@_docstring.interpd class BoxStyle(_Style): """ `BoxStyle` is a container class which defines several @@ -2727,7 +2742,7 @@ def __call__(self, x0, y0, width, height, mutation_size): return Path(saw_vertices, codes) -@_docstring.dedent_interpd +@_docstring.interpd class ConnectionStyle(_Style): """ `ConnectionStyle` is a container class which defines @@ -3149,7 +3164,7 @@ def _point_along_a_line(x0, y0, x1, y1, d): return x2, y2 -@_docstring.dedent_interpd +@_docstring.interpd class ArrowStyle(_Style): """ `ArrowStyle` is a container class which defines several @@ -3886,7 +3901,7 @@ def __str__(self): s = self.__class__.__name__ + "((%g, %g), width=%g, height=%g)" return s % (self._x, self._y, self._width, self._height) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, boxstyle="round", *, mutation_scale=1, mutation_aspect=1, **kwargs): """ @@ -3938,7 +3953,7 @@ def __init__(self, xy, width, height, boxstyle="round", *, self._mutation_aspect = mutation_aspect self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def set_boxstyle(self, boxstyle=None, **kwargs): """ Set the box style, possibly with further attributes. @@ -4138,7 +4153,7 @@ def __str__(self): else: return f"{type(self).__name__}({self._path_original})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, posA=None, posB=None, *, path=None, arrowstyle="simple", connectionstyle="arc3", patchA=None, patchB=None, shrinkA=2, shrinkB=2, @@ -4277,7 +4292,7 @@ def set_patchB(self, patchB): self.patchB = patchB self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def set_connectionstyle(self, connectionstyle=None, **kwargs): """ Set the connection style, possibly with further attributes. @@ -4321,6 +4336,7 @@ def get_connectionstyle(self): """Return the `ConnectionStyle` used.""" return self._connector + @_docstring.interpd def set_arrowstyle(self, arrowstyle=None, **kwargs): """ Set the arrow style, possibly with further attributes. @@ -4464,7 +4480,7 @@ def __str__(self): return "ConnectionPatch((%g, %g), (%g, %g))" % \ (self.xy1[0], self.xy1[1], self.xy2[0], self.xy2[1]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xyA, xyB, coordsA, coordsB=None, *, axesA=None, axesB=None, arrowstyle="-", @@ -4573,29 +4589,34 @@ def _get_xy(self, xy, s, axes=None): s0 = s # For the error message, if needed. if axes is None: axes = self.axes - xy = np.array(xy) + + # preserve mixed type input (such as str, int) + x = np.array(xy[0]) + y = np.array(xy[1]) + + fig = self.get_figure(root=False) if s in ["figure points", "axes points"]: - xy *= self.figure.dpi / 72 + x = x * fig.dpi / 72 + y = y * fig.dpi / 72 s = s.replace("points", "pixels") elif s == "figure fraction": - s = self.figure.transFigure + s = fig.transFigure elif s == "subfigure fraction": - s = self.figure.transSubfigure + s = fig.transSubfigure elif s == "axes fraction": s = axes.transAxes - x, y = xy if s == 'data': trans = axes.transData - x = float(self.convert_xunits(x)) - y = float(self.convert_yunits(y)) + x = cbook._to_unmasked_float_array(axes.xaxis.convert_units(x)) + y = cbook._to_unmasked_float_array(axes.yaxis.convert_units(y)) return trans.transform((x, y)) elif s == 'offset points': if self.xycoords == 'offset points': # prevent recursion return self._get_xy(self.xy, 'data') return ( self._get_xy(self.xy, self.xycoords) # converted data point - + xy * self.figure.dpi / 72) # converted offset + + xy * self.get_figure(root=True).dpi / 72) # converted offset elif s == 'polar': theta, r = x, y x = r * np.cos(theta) @@ -4604,13 +4625,13 @@ def _get_xy(self, xy, s, axes=None): return trans.transform((x, y)) elif s == 'figure pixels': # pixels from the lower left corner of the figure - bb = self.figure.figbbox + bb = self.get_figure(root=False).figbbox x = bb.x0 + x if x >= 0 else bb.x1 + x y = bb.y0 + y if y >= 0 else bb.y1 + y return x, y elif s == 'subfigure pixels': # pixels from the lower left corner of the figure - bb = self.figure.bbox + bb = self.get_figure(root=False).bbox x = bb.x0 + x if x >= 0 else bb.x1 + x y = bb.y0 + y if y >= 0 else bb.y1 + y return x, y diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index f6c9ddf75839..0645479ee5e7 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -59,6 +59,8 @@ class Patch(artist.Artist): def set_joinstyle(self, s: JoinStyleType) -> None: ... def get_joinstyle(self) -> Literal["miter", "round", "bevel"]: ... def set_hatch(self, hatch: str) -> None: ... + def set_hatch_linewidth(self, lw: float) -> None: ... + def get_hatch_linewidth(self) -> float: ... def get_hatch(self) -> str: ... def get_path(self) -> Path: ... diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 94fd97d7b599..5f5a0f3de423 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -129,7 +129,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, vertices = _to_unmasked_float_array(vertices) _api.check_shape((None, 2), vertices=vertices) - if codes is not None: + if codes is not None and len(vertices): codes = np.asarray(codes, self.code_type) if codes.ndim != 1 or len(codes) != len(vertices): raise ValueError("'codes' must be a 1D list or array with the " @@ -1086,10 +1086,7 @@ def get_path_collection_extents( if len(paths) == 0: raise ValueError("No paths provided") if len(offsets) == 0: - _api.warn_deprecated( - "3.8", message="Calling get_path_collection_extents() with an" - " empty offsets list is deprecated since %(since)s. Support will" - " be removed %(removal)s.") + raise ValueError("No offsets provided") extents, minpos = _path.get_path_collection_extents( master_transform, paths, np.atleast_3d(transforms), offsets, offset_transform) diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py index cf159ddc4023..e7a0138bd750 100644 --- a/lib/matplotlib/patheffects.py +++ b/lib/matplotlib/patheffects.py @@ -96,6 +96,13 @@ def __init__(self, path_effects, renderer): def copy_with_path_effect(self, path_effects): return self.__class__(path_effects, self._renderer) + def __getattribute__(self, name): + if name in ['flipy', 'get_canvas_width_height', 'new_gc', + 'points_to_pixels', '_text2path', 'height', 'width']: + return getattr(self._renderer, name) + else: + return object.__getattribute__(self, name) + def draw_path(self, gc, tpath, affine, rgbFace=None): for path_effect in self._path_effects: path_effect.draw_path(self._renderer, gc, tpath, affine, @@ -137,21 +144,6 @@ def draw_path_collection(self, gc, master_transform, paths, *args, renderer.draw_path_collection(gc, master_transform, paths, *args, **kwargs) - def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): - # Implements the naive text drawing as is found in RendererBase. - path, transform = self._get_text_path_transform(x, y, s, prop, - angle, ismath) - color = gc.get_rgb() - gc.set_linewidth(0.0) - self.draw_path(gc, path, transform, rgbFace=color) - - def __getattribute__(self, name): - if name in ['flipy', 'get_canvas_width_height', 'new_gc', - 'points_to_pixels', '_text2path', 'height', 'width']: - return getattr(self._renderer, name) - else: - return object.__getattribute__(self, name) - def open_group(self, s, gid=None): return self._renderer.open_group(s, gid) diff --git a/lib/matplotlib/projections/__init__.py b/lib/matplotlib/projections/__init__.py index b58d1ceb754d..f7b46192a84e 100644 --- a/lib/matplotlib/projections/__init__.py +++ b/lib/matplotlib/projections/__init__.py @@ -123,4 +123,4 @@ def get_projection_class(projection=None): get_projection_names = projection_registry.get_projection_names -_docstring.interpd.update(projection_names=get_projection_names()) +_docstring.interpd.register(projection_names=get_projection_names()) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 498b2f72ebb4..d5ab3c746dea 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -151,6 +151,15 @@ def set_xlim(self, *args, **kwargs): "not supported. Please consider using Cartopy.") set_ylim = set_xlim + set_xbound = set_xlim + set_ybound = set_ylim + + def invert_xaxis(self): + """Not supported. Please consider using Cartopy.""" + raise TypeError("Changing axes limits of a geographic projection is " + "not supported. Please consider using Cartopy.") + + invert_yaxis = invert_xaxis def format_coord(self, lon, lat): """Return a format string formatting the coordinate.""" @@ -249,7 +258,6 @@ class AitoffAxes(GeoAxes): class AitoffTransform(_GeoTransform): """The base Aitoff transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -271,7 +279,6 @@ def inverted(self): class InvertedAitoffTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited # MGDTODO: Math is hard ;( @@ -297,7 +304,6 @@ class HammerAxes(GeoAxes): class HammerTransform(_GeoTransform): """The base Hammer transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -315,7 +321,6 @@ def inverted(self): class InvertedHammerTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T @@ -344,7 +349,6 @@ class MollweideAxes(GeoAxes): class MollweideTransform(_GeoTransform): """The base Mollweide transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited def d(theta): @@ -385,7 +389,6 @@ def inverted(self): class InvertedMollweideTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T @@ -426,7 +429,6 @@ def __init__(self, center_longitude, center_latitude, resolution): self._center_longitude = center_longitude self._center_latitude = center_latitude - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -460,7 +462,6 @@ def __init__(self, center_longitude, center_latitude, resolution): self._center_longitude = center_longitude self._center_latitude = center_latitude - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 8d3e03f64e7c..7fe6045039b1 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -21,7 +21,7 @@ def _apply_theta_transforms_warn(): message=( "Passing `apply_theta_transforms=True` (the default) " "is deprecated since Matplotlib %(since)s. " - "Support for this will be removed in Matplotlib %(removal)s. " + "Support for this will be removed in Matplotlib in %(removal)s. " "To prevent this warning, set `apply_theta_transforms=False`, " "and make sure to shift theta values before being passed to " "this transform." @@ -79,7 +79,6 @@ def _get_rorigin(self): return self._scale_transform.transform( (0, self._axis.get_rorigin()))[1] - @_api.rename_parameter("3.8", "tr", "values") def transform_non_affine(self, values): # docstring inherited theta, r = np.transpose(values) @@ -235,7 +234,6 @@ def __init__(self, axis=None, use_rmin=True, use_rmin="_use_rmin", apply_theta_transforms="_apply_theta_transforms") - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T @@ -341,9 +339,9 @@ class ThetaTick(maxis.XTick): def __init__(self, axes, *args, **kwargs): self._text1_translate = mtransforms.ScaledTranslation( - 0, 0, axes.figure.dpi_scale_trans) + 0, 0, axes.get_figure(root=False).dpi_scale_trans) self._text2_translate = mtransforms.ScaledTranslation( - 0, 0, axes.figure.dpi_scale_trans) + 0, 0, axes.get_figure(root=False).dpi_scale_trans) super().__init__(axes, *args, **kwargs) self.label1.set( rotation_mode='anchor', @@ -530,7 +528,7 @@ class _ThetaShift(mtransforms.ScaledTranslation): of the axes, or using the rlabel position (``'rlabel'``). """ def __init__(self, axes, pad, mode): - super().__init__(pad, pad, axes.figure.dpi_scale_trans) + super().__init__(pad, pad, axes.get_figure(root=False).dpi_scale_trans) self.set_children(axes._realViewLim) self.axes = axes self.mode = mode @@ -1447,12 +1445,25 @@ def format_sig(value, delta, opt, fmt): cbook._g_sig_digits(value, delta)) return f"{value:-{opt}.{prec}{fmt}}" - return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' - '({}\N{DEGREE SIGN}), r={}').format( + # In case fmt_xdata was not specified, resort to default + + if self.fmt_ydata is None: + r_label = format_sig(r, delta_r, "#", "g") + else: + r_label = self.format_ydata(r) + + if self.fmt_xdata is None: + return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' + '({}\N{DEGREE SIGN}), r={}').format( format_sig(theta_halfturns, delta_t_halfturns, "", "f"), format_sig(theta_degrees, delta_t_degrees, "", "f"), - format_sig(r, delta_r, "#", "g"), + r_label ) + else: + return '\N{GREEK SMALL LETTER THETA}={}, r={}'.format( + self.format_xdata(theta), + r_label + ) def get_data_ratio(self): """ diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 442013f7d21a..ba7a5e32f5d0 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -15,6 +15,7 @@ x = np.arange(0, 5, 0.1) y = np.sin(x) plt.plot(x, y) + plt.show() The explicit object-oriented API is recommended for complex plots, though pyplot is still usually used to create the figure and often the Axes in the @@ -29,6 +30,7 @@ y = np.sin(x) fig, ax = plt.subplots() ax.plot(x, y) + plt.show() See :ref:`api_interfaces` for an explanation of the tradeoffs between the @@ -55,8 +57,10 @@ import matplotlib.colorbar import matplotlib.image from matplotlib import _api -from matplotlib import ( # noqa: F401 Re-exported for typing. - cm as cm, get_backend as get_backend, rcParams as rcParams, style as style) +# Re-exported (import x as x) for typing. +from matplotlib import get_backend as get_backend, rcParams as rcParams +from matplotlib import cm as cm # noqa: F401 +from matplotlib import style as style # noqa: F401 from matplotlib import _pylab_helpers from matplotlib import interactive # noqa: F401 from matplotlib import cbook @@ -71,6 +75,7 @@ from matplotlib.axes import Subplot # noqa: F401 from matplotlib.backends import BackendFilter, backend_registry from matplotlib.projections import PolarAxes +from matplotlib.colorizer import _ColorizerInterface, ColorizingArtist, Colorizer from matplotlib import mlab # for detrend_none, window_hanning from matplotlib.scale import get_scale_names # noqa: F401 @@ -95,11 +100,12 @@ import matplotlib.backend_bases from matplotlib.axis import Tick from matplotlib.axes._base import _AxesBase - from matplotlib.backend_bases import RendererBase, Event + from matplotlib.backend_bases import Event from matplotlib.cm import ScalarMappable from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.collections import ( Collection, + FillBetweenPolyCollection, LineCollection, PolyCollection, PathCollection, @@ -119,8 +125,13 @@ from matplotlib.patches import FancyArrow, StepPatch, Wedge from matplotlib.quiver import Barbs, Quiver, QuiverKey from matplotlib.scale import ScaleBase - from matplotlib.transforms import Transform, Bbox - from matplotlib.typing import ColorType, LineStyleType, MarkerType, HashableList + from matplotlib.typing import ( + ColorType, + CoordsType, + HashableList, + LineStyleType, + MarkerType, + ) from matplotlib.widgets import SubplotTool _P = ParamSpec('_P') @@ -409,8 +420,7 @@ def switch_backend(newbackend: str) -> None: switch_backend("agg") rcParamsOrig["backend"] = "agg" return - # have to escape the switch on access logic - old_backend = dict.__getitem__(rcParams, 'backend') + old_backend = rcParams._get('backend') # get without triggering backend resolution module = backend_registry.load_backend_module(newbackend) canvas_class = module.FigureCanvas @@ -509,14 +519,6 @@ def draw_if_interactive() -> None: # See https://github.com/matplotlib/matplotlib/issues/6092 matplotlib.backends.backend = newbackend # type: ignore[attr-defined] - if not cbook._str_equal(old_backend, newbackend): - if get_fignums(): - _api.warn_deprecated("3.8", message=( - "Auto-close()ing of figures upon backend switching is deprecated since " - "%(since)s and will be removed %(removal)s. To suppress this warning, " - "explicitly call plt.close('all') first.")) - close("all") - # Make sure the repl display hook is installed in case we become interactive. install_repl_displayhook() @@ -840,7 +842,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore[arg-type] + stack.callback(rcParams._update_raw, rcParams.copy()) # type: ignore[arg-type] from matplotlib import patheffects rcParams.update({ @@ -985,30 +987,43 @@ def figure( `~matplotlib.rcParams` defines the default values, which can be modified in the matplotlibrc file. """ + allnums = get_fignums() + if isinstance(num, FigureBase): # type narrowed to `Figure | SubFigure` by combination of input and isinstance - if num.canvas.manager is None: + root_fig = num.get_figure(root=True) + if root_fig.canvas.manager is None: raise ValueError("The passed figure is not managed by pyplot") - _pylab_helpers.Gcf.set_active(num.canvas.manager) - return num.figure + elif any([figsize, dpi, facecolor, edgecolor, not frameon, + kwargs]) 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 - allnums = get_fignums() next_num = max(allnums) + 1 if allnums else 1 fig_label = '' if num is None: num = next_num - elif isinstance(num, str): - fig_label = num - all_labels = get_figlabels() - if fig_label not in all_labels: - if fig_label == 'all': - _api.warn_external("close('all') closes all existing figures.") - num = next_num - else: - inum = all_labels.index(fig_label) - num = allnums[inum] else: - num = int(num) # crude validation of num argument + if any([figsize, dpi, facecolor, edgecolor, not frameon, + kwargs]) and num in allnums: + _api.warn_external( + "Ignoring specified arguments in this call " + f"because figure with num: {num} already exists") + if isinstance(num, str): + fig_label = num + all_labels = get_figlabels() + if fig_label not in all_labels: + if fig_label == 'all': + _api.warn_external("close('all') closes all existing figures.") + num = next_num + else: + inum = all_labels.index(fig_label) + num = allnums[inum] + else: + num = int(num) # crude validation of num argument # Type of "num" has narrowed to int, but mypy can't quite see it manager = _pylab_helpers.Gcf.get_fig_manager(num) # type: ignore[arg-type] @@ -1244,7 +1259,7 @@ def figlegend(*args, **kwargs) -> Legend: ## Axes ## -@_docstring.dedent_interpd +@_docstring.interpd def axes( arg: None | tuple[float, float, float, float] = None, **kwargs @@ -1350,8 +1365,9 @@ def sca(ax: Axes) -> None: # Mypy sees ax.figure as potentially None, # but if you are calling this, it won't be None # Additionally the slight difference between `Figure` and `FigureBase` mypy catches - figure(ax.figure) # type: ignore[arg-type] - ax.figure.sca(ax) # type: ignore[union-attr] + fig = ax.get_figure(root=False) + figure(fig) # type: ignore[arg-type] + fig.sca(ax) # type: ignore[union-attr] def cla() -> None: @@ -1362,7 +1378,7 @@ def cla() -> None: ## More ways of creating Axes ## -@_docstring.dedent_interpd +@_docstring.interpd def subplot(*args, **kwargs) -> Axes: """ Add an Axes to the current figure or retrieve an existing Axes. @@ -2501,7 +2517,7 @@ def _get_pyplot_commands() -> list[str]: @_copy_docstring_and_deprecators(Figure.colorbar) def colorbar( - mappable: ScalarMappable | None = None, + mappable: ScalarMappable | ColorizingArtist | None = None, cax: matplotlib.axes.Axes | None = None, ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, **kwargs @@ -2667,17 +2683,32 @@ def polar(*args, **kwargs) -> list[Line2D]: call signature:: - polar(theta, r, **kwargs) + polar(theta, r, [fmt], **kwargs) + + This is a convenience wrapper around `.pyplot.plot`. It ensures that the + current Axes is polar (or creates one if needed) and then passes all parameters + to ``.pyplot.plot``. - Multiple *theta*, *r* arguments are supported, with format strings, as in - `plot`. + .. note:: + When making polar plots using the :ref:`pyplot API `, + ``polar()`` should typically be the first command because that makes sure + a polar Axes is created. Using other commands such as ``plt.title()`` + before this can lead to the implicit creation of a rectangular Axes, in which + case a subsequent ``polar()`` call will fail. """ # If an axis already exists, check if it has a polar projection if gcf().get_axes(): ax = gca() if not isinstance(ax, PolarAxes): - _api.warn_external('Trying to create polar plot on an Axes ' - 'that does not have a polar projection.') + _api.warn_deprecated( + "3.10", + message="There exists a non-polar current Axes. Therefore, the " + "resulting plot from 'polar()' is non-polar. You likely " + "should call 'polar()' before any other pyplot plotting " + "commands. " + "Support for this scenario is deprecated in %(since)s and " + "will raise an error in %(removal)s" + ) else: ax = axes(projection="polar") return ax.plot(*args, **kwargs) @@ -2711,6 +2742,8 @@ def figimage( vmax: float | None = None, origin: Literal["upper", "lower"] | None = None, resize: bool = False, + *, + colorizer: Colorizer | None = None, **kwargs, ) -> FigureImage: return gcf().figimage( @@ -2724,6 +2757,7 @@ def figimage( vmax=vmax, origin=origin, resize=resize, + colorizer=colorizer, **kwargs, ) @@ -2744,7 +2778,7 @@ def gca() -> Axes: # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Figure._gci) -def gci() -> ScalarMappable | None: +def gci() -> ColorizingArtist | None: return gcf()._gci() @@ -2846,17 +2880,8 @@ def annotate( text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = None, - xycoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] = "data", - textcoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] - | None = None, + xycoords: CoordsType = "data", + textcoords: CoordsType | None = None, arrowprops: dict[str, Any] | None = None, annotation_clip: bool | None = None, **kwargs, @@ -3021,6 +3046,7 @@ def boxplot( notch: bool | None = None, sym: str | None = None, vert: bool | None = None, + orientation: Literal["vertical", "horizontal"] = "vertical", whis: float | tuple[float, float] | None = None, positions: ArrayLike | None = None, widths: float | ArrayLike | None = None, @@ -3053,6 +3079,7 @@ def boxplot( notch=notch, sym=sym, vert=vert, + orientation=orientation, whis=whis, positions=positions, widths=widths, @@ -3311,7 +3338,7 @@ def fill_between( *, data=None, **kwargs, -) -> PolyCollection: +) -> FillBetweenPolyCollection: return gca().fill_between( x, y1, @@ -3336,7 +3363,7 @@ def fill_betweenx( *, data=None, **kwargs, -) -> PolyCollection: +) -> FillBetweenPolyCollection: return gca().fill_betweenx( y, x1, @@ -3381,6 +3408,7 @@ def hexbin( reduce_C_function: Callable[[np.ndarray | list[float]], float] = np.mean, mincnt: int | None = None, marginals: bool = False, + colorizer: Colorizer | None = None, *, data=None, **kwargs, @@ -3404,6 +3432,7 @@ def hexbin( reduce_C_function=reduce_C_function, mincnt=mincnt, marginals=marginals, + colorizer=colorizer, **({"data": data} if data is not None else {}), **kwargs, ) @@ -3549,9 +3578,10 @@ def imshow( alpha: float | ArrayLike | None = None, vmin: float | None = None, vmax: float | None = None, + colorizer: Colorizer | None = None, origin: Literal["upper", "lower"] | None = None, extent: tuple[float, float, float, float] | None = None, - interpolation_stage: Literal["data", "rgba"] | None = None, + interpolation_stage: Literal["data", "rgba", "auto"] | None = None, filternorm: bool = True, filterrad: float = 4.0, resample: bool | None = None, @@ -3568,6 +3598,7 @@ def imshow( alpha=alpha, vmin=vmin, vmax=vmax, + colorizer=colorizer, origin=origin, extent=extent, interpolation_stage=interpolation_stage, @@ -3662,6 +3693,7 @@ def pcolor( cmap: str | Colormap | None = None, vmin: float | None = None, vmax: float | None = None, + colorizer: Colorizer | None = None, data=None, **kwargs, ) -> Collection: @@ -3673,6 +3705,7 @@ def pcolor( cmap=cmap, vmin=vmin, vmax=vmax, + colorizer=colorizer, **({"data": data} if data is not None else {}), **kwargs, ) @@ -3689,6 +3722,7 @@ def pcolormesh( cmap: str | Colormap | None = None, vmin: float | None = None, vmax: float | None = None, + colorizer: Colorizer | None = None, shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None, antialiased: bool = False, data=None, @@ -3701,6 +3735,7 @@ def pcolormesh( cmap=cmap, vmin=vmin, vmax=vmax, + colorizer=colorizer, shading=shading, antialiased=antialiased, **({"data": data} if data is not None else {}), @@ -3896,6 +3931,7 @@ def scatter( linewidths: float | Sequence[float] | None = None, *, edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = None, + colorizer: Colorizer | None = None, plotnonfinite: bool = False, data=None, **kwargs, @@ -3913,6 +3949,7 @@ def scatter( alpha=alpha, linewidths=linewidths, edgecolors=edgecolors, + colorizer=colorizer, plotnonfinite=plotnonfinite, **({"data": data} if data is not None else {}), **kwargs, @@ -4002,8 +4039,8 @@ def spy( origin=origin, **kwargs, ) - if isinstance(__ret, cm.ScalarMappable): - sci(__ret) # noqa + if isinstance(__ret, _ColorizerInterface): + sci(__ret) return __ret @@ -4245,7 +4282,8 @@ def triplot(*args, **kwargs): def violinplot( dataset: ArrayLike | Sequence[ArrayLike], positions: ArrayLike | None = None, - vert: bool = True, + vert: bool | None = None, + orientation: Literal["vertical", "horizontal"] = "vertical", widths: float | ArrayLike = 0.5, showmeans: bool = False, showextrema: bool = True, @@ -4264,6 +4302,7 @@ def violinplot( dataset, positions=positions, vert=vert, + orientation=orientation, widths=widths, showmeans=showmeans, showextrema=showextrema, @@ -4328,7 +4367,7 @@ def xcorr( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes._sci) -def sci(im: ScalarMappable) -> None: +def sci(im: ColorizingArtist) -> None: gca()._sci(im) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 8fa1962d6321..e66f1f97b21f 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -32,10 +32,11 @@ Call signature:: - quiver([X, Y], U, V, [C], **kwargs) + quiver([X, Y], U, V, [C], /, **kwargs) *X*, *Y* define the arrow locations, *U*, *V* define the arrow directions, and -*C* optionally sets the color. +*C* optionally sets the color. The arguments *X*, *Y*, *U*, *V*, *C* are +positional-only. **Arrow length** @@ -86,14 +87,19 @@ angles : {'uv', 'xy'} or array-like, default: 'uv' Method for determining the angle of the arrows. - - 'uv': Arrow direction in screen coordinates. Use this if the arrows - symbolize a quantity that is not based on *X*, *Y* data coordinates. + - 'uv': Arrow directions are based on + :ref:`display coordinates `; i.e. a 45° angle will + always show up as diagonal on the screen, irrespective of figure or Axes + aspect ratio or Axes data ranges. This is useful when the arrows represent + a quantity whose direction is not tied to the x and y data coordinates. If *U* == *V* the orientation of the arrow on the plot is 45 degrees counter-clockwise from the horizontal axis (positive to the right). - 'xy': Arrow direction in data coordinates, i.e. the arrows point from - (x, y) to (x+u, y+v). Use this e.g. for plotting a gradient field. + (x, y) to (x+u, y+v). This is ideal for vector fields or gradient plots + where the arrows should directly represent movements or gradients in the + x and y directions. - Arbitrary angles may be specified explicitly as an array of values in degrees, counter-clockwise from the horizontal axis. @@ -101,6 +107,9 @@ In this case *U*, *V* is only used to determine the length of the arrows. + For example, ``angles=[30, 60, 90]`` will orient the arrows at 30, 60, and 90 + degrees respectively, regardless of the *U* and *V* components. + Note: inverting a data axis will correspondingly invert the arrows only with ``angles='xy'``. @@ -113,26 +122,59 @@ scale : float, optional Scales the length of the arrow inversely. - Number of data units per arrow length unit, e.g., m/s per plot width; a - smaller scale parameter makes the arrow longer. Default is *None*. + Number of data values represented by one unit of arrow length on the plot. + For example, if the data represents velocity in meters per second (m/s), the + scale parameter determines how many meters per second correspond to one unit of + arrow length relative to the width of the plot. + Smaller scale parameter makes the arrow longer. + + By default, an autoscaling algorithm is used to scale the arrow length to a + reasonable size, which is based on the average vector length and the number of + vectors. + + The arrow length unit is given by the *scale_units* parameter. + +scale_units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, default: 'width' - If *None*, a simple autoscaling algorithm is used, based on the average - vector length and the number of vectors. The arrow length unit is given by - the *scale_units* parameter. + The physical image unit, which is used for rendering the scaled arrow data *U*, *V*. -scale_units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, optional - If the *scale* kwarg is *None*, the arrow length unit. Default is *None*. + The rendered arrow length is given by - e.g. *scale_units* is 'inches', *scale* is 2.0, and ``(u, v) = (1, 0)``, - then the vector will be 0.5 inches long. + length in x direction = $\\frac{u}{\\mathrm{scale}} \\mathrm{scale_unit}$ - If *scale_units* is 'width' or 'height', then the vector will be half the - width/height of the axes. + 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 + in a horizontal arrow with a length of *0.5 / 10 * "width"*, i.e. 0.05 times the + Axes width. + + Supported values are: - If *scale_units* is 'x' then the vector will be 0.5 x-axis - units. To plot vectors in the x-y plane, with u and v having - the same units as x and y, use - ``angles='xy', scale_units='xy', scale=1``. + - 'width' or 'height': The arrow length is scaled relative to the width or height + of the Axes. + For example, ``scale_units='width', scale=1.0``, will result in an arrow length + of width of the Axes. + + - 'dots': The arrow length of the arrows is in measured in display dots (pixels). + + - 'inches': Arrow lengths are scaled based on the DPI (dots per inch) of the figure. + This ensures that the arrows have a consistent physical size on the figure, + in inches, regardless of data values or plot scaling. + For example, ``(u, v) = (1, 0)`` with ``scale_units='inches', scale=2`` results + in a 0.5 inch-long arrow. + + - 'x' or 'y': The arrow length is scaled relative to the x or y axis units. + For example, ``(u, v) = (0, 1)`` with ``scale_units='x', scale=1`` results + in a vertical arrow with the length of 1 x-axis unit. + + - 'xy': Arrow length will be same as 'x' or 'y' units. + This is useful for creating vectors in the x-y plane where u and v have + the same units as x and y. To plot vectors in the x-y plane with u and v having + the same units as x and y, use ``angles='xy', scale_units='xy', scale=1``. + + Note: Setting *scale_units* without setting scale does not have any effect because + the scale units only differ by a constant factor and that is rescaled through + autoscaling. units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, default: 'width' Affects the arrow size (except for the length). In particular, the shaft @@ -229,7 +271,7 @@ of the head in forward direction so that the arrow head looks broken. """ % _docstring.interpd.params -_docstring.interpd.update(quiver_doc=_quiver_doc) +_docstring.interpd.register(quiver_doc=_quiver_doc) class QuiverKey(martist.Artist): @@ -240,7 +282,8 @@ class QuiverKey(martist.Artist): def __init__(self, Q, X, Y, U, label, *, angle=0, coordinates='axes', color=None, labelsep=0.1, - labelpos='N', labelcolor=None, fontproperties=None, **kwargs): + labelpos='N', labelcolor=None, fontproperties=None, + zorder=None, **kwargs): """ Add a key to a quiver plot. @@ -284,6 +327,8 @@ def __init__(self, Q, X, Y, U, label, A dictionary with keyword arguments accepted by the `~matplotlib.font_manager.FontProperties` initializer: *family*, *style*, *variant*, *size*, *weight*. + zorder : float + The zorder of the key. The default is 0.1 above *Q*. **kwargs Any additional keyword arguments are used to override vector properties taken from *Q*. @@ -311,15 +356,15 @@ def __init__(self, Q, X, Y, U, label, if self.labelcolor is not None: self.text.set_color(self.labelcolor) self._dpi_at_last_init = None - self.zorder = Q.zorder + 0.1 + self.zorder = zorder if zorder is not None else Q.zorder + 0.1 @property def labelsep(self): - return self._labelsep_inches * self.Q.axes.figure.dpi + return self._labelsep_inches * self.Q.axes.get_figure(root=True).dpi def _init(self): - if True: # self._dpi_at_last_init != self.axes.figure.dpi - if self.Q._dpi_at_last_init != self.Q.axes.figure.dpi: + if True: # self._dpi_at_last_init != self.axes.get_figure().dpi + if self.Q._dpi_at_last_init != self.Q.axes.get_figure(root=True).dpi: self.Q._init() self._set_transform() with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], @@ -340,7 +385,7 @@ def _init(self): self.vector.set_color(self.color) self.vector.set_transform(self.Q.get_transform()) self.vector.set_figure(self.get_figure()) - self._dpi_at_last_init = self.Q.axes.figure.dpi + self._dpi_at_last_init = self.Q.axes.get_figure(root=True).dpi def _text_shift(self): return { @@ -360,11 +405,12 @@ def draw(self, renderer): self.stale = False def _set_transform(self): + fig = self.Q.axes.get_figure(root=False) self.set_transform(_api.check_getitem({ "data": self.Q.axes.transData, "axes": self.Q.axes.transAxes, - "figure": self.Q.axes.figure.transFigure, - "inches": self.Q.axes.figure.dpi_scale_trans, + "figure": fig.transFigure, + "inches": fig.dpi_scale_trans, }, coordinates=self.coord)) def set_figure(self, fig): @@ -423,13 +469,13 @@ def _parse_args(*args, caller_name='function'): X = X.ravel() Y = Y.ravel() if len(X) == nc and len(Y) == nr: - X, Y = [a.ravel() for a in np.meshgrid(X, Y)] + X, Y = (a.ravel() for a in np.meshgrid(X, Y)) elif len(X) != len(Y): raise ValueError('X and Y must be the same size, but ' f'X.size is {X.size} and Y.size is {Y.size}.') else: indexgrid = np.meshgrid(np.arange(nc), np.arange(nr)) - X, Y = [np.ravel(a) for a in indexgrid] + X, Y = (np.ravel(a) for a in indexgrid) # Size validation for U, V, C is left to the set_UVC method. return X, Y, U, V, C @@ -517,11 +563,11 @@ def _init(self): self.width = 0.06 * self.span / sn # _make_verts sets self.scale if not already specified - if (self._dpi_at_last_init != self.axes.figure.dpi + if (self._dpi_at_last_init != self.axes.get_figure(root=True).dpi and self.scale is None): self._make_verts(self.XY, self.U, self.V, self.angles) - self._dpi_at_last_init = self.axes.figure.dpi + self._dpi_at_last_init = self.axes.get_figure(root=True).dpi def get_datalim(self, transData): trans = self.get_transform() @@ -578,7 +624,7 @@ def _dots_per_unit(self, units): 'width': bb.width, 'height': bb.height, 'dots': 1., - 'inches': self.axes.figure.dpi, + 'inches': self.axes.get_figure(root=True).dpi, }, units=units) def _set_transform(self): @@ -731,13 +777,14 @@ def _h_arrows(self, length): Call signature:: - barbs([X, Y], U, V, [C], **kwargs) + barbs([X, Y], U, V, [C], /, **kwargs) Where *X*, *Y* define the barb locations, *U*, *V* define the barb directions, and *C* optionally sets the color. -All arguments may be 1D or 2D. *U*, *V*, *C* may be masked arrays, but masked -*X*, *Y* are not supported at present. +The arguments *X*, *Y*, *U*, *V*, *C* are positional-only and may be +1D or 2D. *U*, *V*, *C* may be masked arrays, but masked *X*, *Y* +are not supported at present. Barbs are traditionally used in meteorology as a way to plot the speed and direction of wind observations, but can technically be used to @@ -862,7 +909,7 @@ def _h_arrows(self, length): %(PolyCollection:kwdoc)s """ % _docstring.interpd.params -_docstring.interpd.update(barbs_doc=_barbs_doc) +_docstring.interpd.register(barbs_doc=_barbs_doc) class Barbs(mcollections.PolyCollection): diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index 2a043a92b4b5..8a14083c4348 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -1,7 +1,7 @@ import matplotlib.artist as martist import matplotlib.collections as mcollections from matplotlib.axes import Axes -from matplotlib.figure import Figure +from matplotlib.figure import Figure, SubFigure from matplotlib.text import Text from matplotlib.transforms import Transform, Bbox @@ -45,11 +45,12 @@ class QuiverKey(martist.Artist): labelpos: Literal["N", "S", "E", "W"] = ..., labelcolor: ColorType | None = ..., fontproperties: dict[str, Any] | None = ..., + zorder: float | None = ..., **kwargs ) -> None: ... @property def labelsep(self) -> float: ... - def set_figure(self, fig: Figure) -> None: ... + def set_figure(self, fig: Figure | SubFigure) -> None: ... class Quiver(mcollections.PolyCollection): X: ArrayLike diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b0cd22098489..c23d9f818454 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -463,19 +463,6 @@ def validate_ps_distiller(s): return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s) -def _validate_papersize(s): - # Re-inline this validator when the 'auto' deprecation expires. - s = ValidateInStrings("ps.papersize", - ["figure", "auto", "letter", "legal", "ledger", - *[f"{ab}{i}" for ab in "ab" for i in range(11)]], - ignorecase=True)(s) - if s == "auto": - _api.warn_deprecated("3.8", name="ps.papersize='auto'", - addendum="Pass an explicit paper type, figure, or omit " - "the *ps.papersize* rcParam entirely.") - return s - - # A validator dedicated to the named line styles, based on the items in # ls_mapper, and a list of possible strings read from Line2D.set_linestyle _validate_named_linestyle = ValidateInStrings( @@ -695,7 +682,7 @@ def cycler(*args, **kwargs): Call signatures:: cycler(cycler) - cycler(label=values[, label2=values2[, ...]]) + cycler(label=values, label2=values2, ...) cycler(label, values) Form 1 copies a given `~cycler.Cycler` object. @@ -1053,7 +1040,7 @@ def _convert_validator_spec(key, conv): "image.aspect": validate_aspect, # equal, auto, a number "image.interpolation": validate_string, - "image.interpolation_stage": ["data", "rgba"], + "image.interpolation_stage": ["auto", "data", "rgba"], "image.cmap": _validate_cmap, # gray, jet, etc. "image.lut": validate_int, # lookup table "image.origin": ["upper", "lower"], @@ -1132,6 +1119,10 @@ def _convert_validator_spec(key, conv): "axes3d.yaxis.panecolor": validate_color, # 3d background pane "axes3d.zaxis.panecolor": validate_color, # 3d background pane + "axes3d.mouserotationstyle": ["azel", "trackball", "sphere", "arcball"], + "axes3d.trackballsize": validate_float, + "axes3d.trackballborder": validate_float, + # scatter props "scatter.marker": _validate_marker, "scatter.edgecolors": validate_string, @@ -1291,7 +1282,9 @@ def _convert_validator_spec(key, conv): "tk.window_focus": validate_bool, # Maintain shell focus for TkAgg # Set the papersize/type - "ps.papersize": _validate_papersize, + "ps.papersize": _ignorecase( + ["figure", "letter", "legal", "ledger", + *[f"{ab}{i}" for ab in "ab" for i in range(11)]]), "ps.useafm": validate_bool, # use ghostscript or xpdf to distill ps output "ps.usedistiller": validate_ps_distiller, @@ -1311,6 +1304,7 @@ def _convert_validator_spec(key, conv): "svg.image_inline": validate_bool, "svg.fonttype": ["none", "path"], # save text as text ("none") or "paths" "svg.hashsalt": validate_string_or_None, + "svg.id": validate_string_or_None, # set this when you want to generate hardcopy docstring "docstring.hardcopy": validate_bool, diff --git a/lib/matplotlib/sankey.py b/lib/matplotlib/sankey.py index 665b9d6deba2..637cfc849f9d 100644 --- a/lib/matplotlib/sankey.py +++ b/lib/matplotlib/sankey.py @@ -347,7 +347,7 @@ def _revert(self, path, first_action=Path.LINETO): # path[2] = path[2][::-1] # return path - @_docstring.dedent_interpd + @_docstring.interpd def add(self, patchlabel='', flows=None, orientations=None, labels='', trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0), rotation=0, **kwargs): diff --git a/lib/matplotlib/sankey.pyi b/lib/matplotlib/sankey.pyi index 4a40c31e3c6a..33565b998a9c 100644 --- a/lib/matplotlib/sankey.pyi +++ b/lib/matplotlib/sankey.pyi @@ -2,6 +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 import numpy as np @@ -56,6 +57,5 @@ class Sankey: connect: tuple[int, int] = ..., rotation: float = ..., **kwargs - # Replace return with Self when py3.9 is dropped - ) -> Sankey: ... + ) -> Self: ... def finish(self) -> list[Any]: ... diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 7f90362b574b..ccaaae6caf5d 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -213,7 +213,6 @@ def __str__(self): return "{}(base={}, nonpositive={!r})".format( type(self).__name__, self.base, "clip" if self._clip else "mask") - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): # Ignore invalid values due to nans being passed to the transform. with np.errstate(divide="ignore", invalid="ignore"): @@ -250,7 +249,6 @@ def __init__(self, base): def __str__(self): return f"{type(self).__name__}(base={self.base})" - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return np.power(self.base, values) @@ -362,7 +360,6 @@ def __init__(self, base, linthresh, linscale): self._linscale_adj = (linscale / (1.0 - self.base ** -1)) self._log_base = np.log(base) - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): abs_a = np.abs(values) with np.errstate(divide="ignore", invalid="ignore"): @@ -390,7 +387,6 @@ def __init__(self, base, linthresh, linscale): self.linscale = linscale self._linscale_adj = (linscale / (1.0 - self.base ** -1)) - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): abs_a = np.abs(values) with np.errstate(divide="ignore", invalid="ignore"): @@ -472,7 +468,6 @@ def __init__(self, linear_width): "must be strictly positive") self.linear_width = linear_width - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return self.linear_width * np.arcsinh(values / self.linear_width) @@ -488,7 +483,6 @@ def __init__(self, linear_width): super().__init__() self.linear_width = linear_width - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return self.linear_width * np.sinh(values / self.linear_width) @@ -589,7 +583,6 @@ def __init__(self, nonpositive='mask'): self._nonpositive = nonpositive self._clip = {"clip": True, "mask": False}[nonpositive] - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): """logit transform (base 10), masked or clipped""" with np.errstate(divide="ignore", invalid="ignore"): @@ -613,7 +606,6 @@ def __init__(self, nonpositive='mask'): super().__init__() self._nonpositive = nonpositive - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): """logistic transform (base 10)""" return 1.0 / (1 + 10**(-values)) @@ -750,7 +742,7 @@ def _get_scale_docs(): return "\n".join(docs) -_docstring.interpd.update( +_docstring.interpd.register( scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]), scale_docs=_get_scale_docs().rstrip(), ) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 39cb99c53d72..7e77a393f2a2 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -32,7 +32,7 @@ class Spine(mpatches.Patch): def __str__(self): return "Spine" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, axes, spine_type, path, **kwargs): """ Parameters @@ -53,7 +53,7 @@ def __init__(self, axes, spine_type, path, **kwargs): """ super().__init__(**kwargs) self.axes = axes - self.set_figure(self.axes.figure) + self.set_figure(self.axes.get_figure(root=False)) self.spine_type = spine_type self.set_facecolor('none') self.set_edgecolor(mpl.rcParams['axes.edgecolor']) @@ -174,8 +174,9 @@ def get_window_extent(self, renderer=None): else: padout = 0.5 padin = 0.5 - padout = padout * tickl / 72 * self.figure.dpi - padin = padin * tickl / 72 * self.figure.dpi + dpi = self.get_figure(root=True).dpi + padout = padout * tickl / 72 * dpi + padin = padin * tickl / 72 * dpi if tick.tick1line.get_visible(): if self.spine_type == 'left': @@ -368,7 +369,7 @@ def get_spine_transform(self): offset_dots = amount * np.array(offset_vec) / 72 return (base_transform + mtransforms.ScaledTranslation( - *offset_dots, self.figure.dpi_scale_trans)) + *offset_dots, self.get_figure(root=False).dpi_scale_trans)) elif position_type == 'axes': if self.spine_type in ['left', 'right']: # keep y unchanged, fix x at amount diff --git a/lib/matplotlib/spines.pyi b/lib/matplotlib/spines.pyi index 0f06a6d1ce2b..ff2a1a40bf94 100644 --- a/lib/matplotlib/spines.pyi +++ b/lib/matplotlib/spines.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable, Iterator, MutableMapping -from typing import Any, Literal, TypeVar, overload +from typing import Literal, TypeVar, overload import matplotlib.patches as mpatches from matplotlib.axes import Axes diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 7e9008c56165..e36c3c37a882 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -12,19 +12,12 @@ """ import contextlib +import importlib.resources import logging import os from pathlib import Path -import sys import warnings -if sys.version_info >= (3, 10): - import importlib.resources as importlib_resources -else: - # Even though Py3.9 has importlib.resources, it doesn't properly handle - # modules added in sys.path. - import importlib_resources - import matplotlib as mpl from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault @@ -121,8 +114,7 @@ def use(style): elif "." in style: pkg, _, name = style.rpartition(".") try: - path = (importlib_resources.files(pkg) - / f"{name}.{STYLE_EXTENSION}") + path = importlib.resources.files(pkg) / f"{name}.{STYLE_EXTENSION}" style = _rc_params_in_file(path) except (ModuleNotFoundError, OSError, TypeError) as exc: # There is an ambiguity whether a dotted name refers to a diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 7d8c8ec4c3f4..21518c4c6726 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -33,6 +33,8 @@ from .transforms import Bbox from .path import Path +from .cbook import _is_pandas_dataframe + class Cell(Rectangle): """ @@ -103,7 +105,6 @@ def __init__(self, xy, width, height, *, text=text, fontproperties=fontproperties, horizontalalignment=loc, verticalalignment='center') - @_api.rename_parameter("3.8", "trans", "t") def set_transform(self, t): super().set_transform(t) # the text does not get the transform! @@ -176,7 +177,7 @@ def get_required_width(self, renderer): l, b, w, h = self.get_text_bounds(renderer) return w * (1.0 + (2.0 * self.PAD)) - @_docstring.dedent_interpd + @_docstring.interpd def set_text_props(self, **kwargs): """ Update the text properties. @@ -303,7 +304,7 @@ def __init__(self, ax, loc=None, bbox=None, **kwargs): "Unrecognized location {!r}. Valid locations are\n\t{}" .format(loc, '\n\t'.join(self.codes))) loc = self.codes[loc] - self.set_figure(ax.figure) + self.set_figure(ax.get_figure(root=False)) self._axes = ax self._loc = loc self._bbox = bbox @@ -354,7 +355,7 @@ def __setitem__(self, position, cell): except Exception as err: raise KeyError('Only tuples length 2 are accepted as ' 'coordinates') from err - cell.set_figure(self.figure) + cell.set_figure(self.get_figure(root=False)) cell.set_transform(self.get_transform()) cell.set_clip_on(False) self._cells[row, col] = cell @@ -389,7 +390,7 @@ def edges(self, value): self.stale = True def _approx_text_height(self): - return (self.FONTSIZE / 72.0 * self.figure.dpi / + return (self.FONTSIZE / 72.0 * self.get_figure(root=True).dpi / self._axes.bbox.height * 1.2) @allow_rasterization @@ -399,7 +400,7 @@ def draw(self, renderer): # Need a renderer to do hit tests on mouseevent; assume the last one # will do if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if renderer is None: raise RuntimeError('No renderer defined') @@ -432,7 +433,7 @@ def contains(self, mouseevent): return False, {} # TODO: Return index of the cell containing the cursor so that the user # doesn't have to bind to each one individually. - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if renderer is not None: boxes = [cell.get_window_extent(renderer) for (row, col), cell in self._cells.items() @@ -449,7 +450,7 @@ def get_children(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self._update_positions(renderer) boxes = [cell.get_window_extent(renderer) for cell in self._cells.values()] @@ -497,11 +498,7 @@ def auto_set_column_width(self, col): """ col1d = np.atleast_1d(col) if not np.issubdtype(col1d.dtype, np.integer): - _api.warn_deprecated("3.8", name="col", - message="%(name)r must be an int or sequence of ints. " - "Passing other types is deprecated since %(since)s " - "and will be removed %(removal)s.") - return + raise TypeError("col must be an int or sequence of ints.") for cell in col1d: self._autoColumns.append(cell) @@ -650,7 +647,7 @@ def get_celld(self): return self._cells -@_docstring.dedent_interpd +@_docstring.interpd def table(ax, cellText=None, cellColours=None, cellLoc='right', colWidths=None, @@ -675,7 +672,7 @@ def table(ax, Parameters ---------- - cellText : 2D list of str, optional + cellText : 2D list of str or pandas.DataFrame, optional The texts to place into the table cells. *Note*: Line breaks in the strings are currently not accounted for and @@ -745,6 +742,21 @@ def table(ax, cols = len(cellColours[0]) cellText = [[''] * cols] * rows + # Check if we have a Pandas DataFrame + if _is_pandas_dataframe(cellText): + # if rowLabels/colLabels are empty, use DataFrame entries. + # Otherwise, throw an error. + if rowLabels is None: + rowLabels = cellText.index + else: + raise ValueError("rowLabels cannot be used alongside Pandas DataFrame") + if colLabels is None: + colLabels = cellText.columns + else: + raise ValueError("colLabels cannot be used alongside Pandas DataFrame") + # Update cellText with only values + cellText = cellText.values + rows = len(cellText) cols = len(cellText[0]) for row in cellText: diff --git a/lib/matplotlib/table.pyi b/lib/matplotlib/table.pyi index 0108ecd99f89..07d2427f66dc 100644 --- a/lib/matplotlib/table.pyi +++ b/lib/matplotlib/table.pyi @@ -8,7 +8,9 @@ from .transforms import Bbox from .typing import ColorType from collections.abc import Sequence -from typing import Any, Literal +from typing import Any, Literal, TYPE_CHECKING + +from pandas import DataFrame class Cell(Rectangle): PAD: float @@ -68,7 +70,7 @@ class Table(Artist): def table( ax: Axes, - cellText: Sequence[Sequence[str]] | None = ..., + cellText: Sequence[Sequence[str]] | DataFrame | None = ..., cellColours: Sequence[Sequence[ColorType]] | None = ..., cellLoc: Literal["left", "center", "right"] = ..., colWidths: Sequence[float] | None = ..., diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index ee93061480e7..0f252bc1da8e 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -13,6 +13,7 @@ import sys from tempfile import TemporaryDirectory, TemporaryFile import weakref +import re import numpy as np from PIL import Image @@ -305,7 +306,15 @@ def convert(filename, cache): # re-generate any SVG test files using this mode, or else such tests will # fail to use the converter for the expected images (but will for the # results), and the tests will fail strangely. - if 'style="font:' in contents: + if re.search( + # searches for attributes : + # style=[font|font-size|font-weight| + # font-family|font-variant|font-style] + # taking care of the possibility of multiple style attributes + # before the font styling (i.e. opacity) + r'style="[^"]*font(|-size|-weight|-family|-variant|-style):', + contents # raw contents of the svg file + ): # for svg.fonttype = none, we explicitly patch the font search # path so that fonts shipped by Matplotlib are found. convert = _svg_with_matplotlib_fonts_converter diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index c285c247e7b4..3f96de611195 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -82,7 +82,20 @@ def mpl_test_settings(request): @pytest.fixture def pd(): - """Fixture to import and configure pandas.""" + """ + Fixture to import and configure pandas. Using this fixture, the test is skipped when + pandas is not installed. Use this fixture instead of importing pandas in test files. + + Examples + -------- + Request the pandas fixture by passing in ``pd`` as an argument to the test :: + + def test_matshow_pandas(pd): + + df = pd.DataFrame({'x':[1,2,3], 'y':[4,5,6]}) + im = plt.figure().subplots().matshow(df) + np.testing.assert_array_equal(im.get_array(), df) + """ pd = pytest.importorskip('pandas') try: from pandas.plotting import ( @@ -95,6 +108,20 @@ def pd(): @pytest.fixture def xr(): - """Fixture to import xarray.""" + """ + Fixture to import xarray so that the test is skipped when xarray is not installed. + Use this fixture instead of importing xrray in test files. + + Examples + -------- + Request the xarray fixture by passing in ``xr`` as an argument to the test :: + + def test_imshow_xarray(xr): + + ds = xr.DataArray(np.random.randn(2, 3)) + im = plt.figure().subplots().imshow(ds) + np.testing.assert_array_equal(im.get_array(), ds) + """ + xr = pytest.importorskip('xarray') return xr diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 49ac4a1506f8..6f1af7debdb3 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -263,7 +263,7 @@ def image_comparison(baseline_images, extensions=None, tol=0, style=("classic", "_classic_test_patch")): """ Compare images generated by the test with those specified in - *baseline_images*, which must correspond, else an `ImageComparisonFailure` + *baseline_images*, which must correspond, else an `.ImageComparisonFailure` exception will be raised. Parameters diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index 748cdaccc7e9..3962567aa7c0 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -16,7 +16,7 @@ def get_ax(): fig, ax = plt.subplots(1, 1) ax.plot([0, 200], [0, 200]) ax.set_aspect(1.0) - ax.figure.canvas.draw() + fig.canvas.draw() return ax @@ -57,7 +57,7 @@ def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): (xdata, ydata)])[0] event.xdata, event.ydata = xdata, ydata event.inaxes = ax - event.canvas = ax.figure.canvas + event.canvas = ax.get_figure(root=True).canvas event.key = key event.step = step event.guiEvent = None diff --git a/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg b/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg index 54ce8e9308f5..d3260ae11ee6 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:36.965429 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,461 +35,476 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #0000ff; opacity: 0.25; stroke: #000000; stroke-linejoin: miter"/> +" 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"/> - + - - + + - + - - - - + - - - - - +" transform="scale(0.015625)"/> + + + + + + + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - + - + - - - - + + + + - - - - - - - - - - + + + + + + + + + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - - + + - + - - +" transform="scale(0.015625)"/> + - + @@ -486,225 +512,233 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + @@ -712,8 +746,8 @@ Q 18.3125 60.0625 18.3125 54.390625 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.pdf b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.pdf index cc9433bebd31..c424bc5e982f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.png b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.png index 07b8a5da7247..944f9451285c 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.png and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.svg b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.svg index 054af5b4f2f3..c3a3cda7a9a0 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_rc_parameters.svg @@ -1,538 +1,535 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2024-04-17T16:38:51.018485 + image/svg+xml + + + Matplotlib v3.9.0.dev1517+g1fa7dd164e.d20240417, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg index e3940abeca5b..361aa8826a73 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:59.370109 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,120 +35,120 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - - +" transform="scale(0.015625)"/> + + + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + @@ -533,654 +554,681 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - - + + + - - - - + - - + - - - - + - + - - - + - + - - +" transform="scale(0.015625)"/> + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg index 8ea6f573d94b..d7f2f4a27f92 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:37:00.187406 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,412 +35,698 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #008000"/> - - + + - + - + - + + - + - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #008000"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000"/> - - + - + - + + - + + - + - + - + + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000"/> - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff00ff"/> - - + - + + + - + + + - + - - - - - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff00ff"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + - + - - - - - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff"/> - - + - + - + - + + - + - + + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff"/> - - - - - - - + - +" style="stroke: #00ffff; stroke-width: 0.5; stroke-linejoin: miter"/> - - - - - - - - - - - + + + + - - - - - - - - - - - - + + + + + + + + - + - +" style="stroke: #00ffff; stroke-width: 0.5; stroke-linejoin: miter"/> - - - - - - - - - - - + + - - - - - - - - - - - - + + + + + + - - + - - - - - - - - - - - - - - - - - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #0000ff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #008000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #ff0000"/> - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #ff00ff"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - - - + + + + + + + + + + + +" 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"/> - + - - - - + + + + @@ -907,32 +904,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -940,42 +938,43 @@ z - + - + - - - - + + + + @@ -983,50 +982,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -1034,36 +1034,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -1071,43 +1073,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -1117,557 +1120,583 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + - + - - + - - - + - - - - - + - - + + - - + - - - +M 3116 1747 +Q 3116 2381 2855 2742 +Q 2594 3103 2138 3103 +Q 1681 3103 1420 2742 +Q 1159 2381 1159 1747 +Q 1159 1113 1420 752 +Q 1681 391 2138 391 +Q 2594 391 2855 752 +Q 3116 1113 3116 1747 +z +" transform="scale(0.015625)"/> + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg index b4d97d7d0e9f..d28b10e07376 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg @@ -6,11 +6,11 @@ - 2022-01-07T01:42:44.033823 + 2024-07-17T16:09:13.200518 image/svg+xml - Matplotlib v3.6.0.dev1138+gd48fca95df.d20220107, https://matplotlib.org/ + Matplotlib v0.1.0.dev50528+ga1cfe8b, https://matplotlib.org/ @@ -40,28 +40,28 @@ z +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> @@ -69,7 +69,7 @@ L 214.036364 121.211578 L -3 -0 " style="stroke: #0000ff; stroke-width: 0.5"/> - + @@ -81,7 +81,7 @@ L -3 -0 - + @@ -106,7 +106,7 @@ C -1.55874 2.683901 -0.795609 3 0 3 z " style="stroke: #000000; stroke-width: 0.5"/> - + @@ -233,7 +233,7 @@ L -4 0 - + - - - + + + @@ -316,10 +316,10 @@ z - + - - + + @@ -336,10 +336,10 @@ z - + - - + + @@ -356,7 +356,7 @@ z - + - - + + @@ -392,17 +392,17 @@ z - + - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -621,28 +621,28 @@ z +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> @@ -650,7 +650,7 @@ L 472.224823 195.998601 L 0 -3 " style="stroke: #0000ff; stroke-opacity: 0.4; stroke-width: 0.5"/> - + @@ -662,7 +662,7 @@ L 0 -3 - + @@ -687,7 +687,7 @@ C -1.55874 2.683901 -0.795609 3 0 3 z " style="stroke: #000000; stroke-opacity: 0.4; stroke-width: 0.5"/> - + @@ -794,10 +794,10 @@ L 518.4 43.2 - + - - + + @@ -814,7 +814,7 @@ L 518.4 43.2 - + - - + + @@ -860,7 +860,7 @@ z - + - - + + @@ -901,7 +901,7 @@ z - + - - + + @@ -953,7 +953,7 @@ z - + - - + + @@ -1014,17 +1014,17 @@ z - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -1209,54 +1209,54 @@ z +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> @@ -1264,7 +1264,7 @@ L 214.036364 272.060219 L 0 -3 " style="stroke: #0000ff; stroke-width: 0.5"/> - + @@ -1276,7 +1276,7 @@ L 0 -3 - + @@ -1288,7 +1288,7 @@ L 0 -3 - + @@ -1300,7 +1300,7 @@ L 0 -3 - + @@ -1320,8 +1320,8 @@ L 175.990909 339.908877 L 188.672727 343.693421 L 201.354545 345.988863 L 214.036364 347.381119 -" clip-path="url(#p2c9259b7f3)" style="fill: none; stroke-dasharray: 6,6; stroke-dashoffset: 0; stroke: #0000ff"/> - +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke-dasharray: 6,6; stroke-dashoffset: 0; stroke: #0000ff"/> + @@ -1366,9 +1366,9 @@ L 274.909091 231.709091 - + - + @@ -1385,7 +1385,7 @@ L 274.909091 231.709091 - + @@ -1403,7 +1403,7 @@ L 274.909091 231.709091 - + @@ -1421,7 +1421,7 @@ L 274.909091 231.709091 - + @@ -1439,7 +1439,7 @@ L 274.909091 231.709091 - + @@ -1459,11 +1459,11 @@ L 274.909091 231.709091 - + - - - + + + @@ -1480,10 +1480,10 @@ L 274.909091 231.709091 - + - - + + @@ -1500,10 +1500,10 @@ L 274.909091 231.709091 - + - - + + @@ -1520,10 +1520,10 @@ L 274.909091 231.709091 - + - - + + @@ -1540,17 +1540,17 @@ L 274.909091 231.709091 - + - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -1592,54 +1592,54 @@ z +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> @@ -1647,7 +1647,7 @@ L 457.527273 284.387119 L 0 -3 " style="stroke: #008000; stroke-width: 2"/> - + @@ -1659,7 +1659,7 @@ L 0 -3 - + @@ -1676,7 +1676,7 @@ L 0 -3 L -3 -0 " style="stroke: #008000; stroke-width: 2"/> - + @@ -1688,7 +1688,7 @@ L -3 -0 - + @@ -1700,7 +1700,7 @@ L -3 -0 - + @@ -1745,9 +1745,9 @@ L 518.4 231.709091 - + - + @@ -1764,7 +1764,7 @@ L 518.4 231.709091 - + @@ -1782,7 +1782,7 @@ L 518.4 231.709091 - + @@ -1800,7 +1800,7 @@ L 518.4 231.709091 - + @@ -1818,7 +1818,7 @@ L 518.4 231.709091 - + @@ -1837,12 +1837,12 @@ L 518.4 231.709091 - - + + - - + + @@ -1858,12 +1858,12 @@ L 518.4 231.709091 - - + + - - + + @@ -1880,10 +1880,10 @@ L 518.4 231.709091 - + - + @@ -1900,10 +1900,10 @@ L 518.4 231.709091 - + - + @@ -2208,7 +2208,7 @@ L -2 0 - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + - + - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg b/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg index c1a886b6ff5f..77cfb8afaffa 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:38.117527 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + @@ -387,149 +405,152 @@ Q 46.96875 40.921875 40.578125 39.3125 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -537,17 +558,17 @@ z - + - + - + @@ -556,8 +577,8 @@ z - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg b/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg index 5f940bb5a83c..de3b541c4f8a 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:36.453826 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,11 +35,11 @@ L 518.4 200.290909 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - - + + +" 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(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -410,224 +389,196 @@ L 518.4 43.2 - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -640,324 +591,302 @@ L 518.4 388.8 L 518.4 231.709091 L 72 231.709091 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(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -965,211 +894,183 @@ L 518.4 231.709091 - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -1177,11 +1078,11 @@ L 518.4 231.709091 - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg b/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg index 37a6b88f3e73..12763588c0d5 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:34.083981 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,517 +35,535 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - + - - - - - + + + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -542,12 +571,12 @@ Q 45.0625 54.296875 48.78125 52.59375 - + - + @@ -555,38 +584,40 @@ Q 45.0625 54.296875 48.78125 52.59375 - + - - - - + + + + @@ -594,33 +625,35 @@ Q 48.6875 17.390625 48.6875 27.296875 - + - - - - + + + + @@ -630,367 +663,381 @@ Q 18.84375 56 30.609375 56 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - + - + - - - +" transform="scale(0.015625)"/> + + - - - - - + + + + + - + - - - - - - + + + + + + - - - + + + - + - - + + - + - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + - - - + + + @@ -1000,116 +1047,116 @@ z +" 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"/> - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + @@ -1119,116 +1166,116 @@ L 0 4 +" 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"/> - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + @@ -1236,8 +1283,8 @@ L -4 0 - - + + 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 new file mode 100644 index 000000000000..c1c8074ed80c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/use_colorizer_keyword.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_ttconv/truetype-conversion.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/truetype-conversion.pdf similarity index 100% rename from lib/matplotlib/tests/baseline_images/test_ttconv/truetype-conversion.pdf rename to lib/matplotlib/tests/baseline_images/test_backend_pdf/truetype-conversion.pdf diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_document_font_size.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_document_font_size.pdf new file mode 100644 index 000000000000..9f060419a2a7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_document_font_size.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg index dea1649a020e..9ceeb930cef2 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.264474 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,103 +35,105 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -128,188 +141,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -318,495 +339,521 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg index ed927a817852..aac64d958b31 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:25.382570 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,425 +284,448 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + + - + - - - + - - - - - + + - + + - - - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg index 02e2f614bd11..c4b5c08c50c0 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.434024 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,106 +35,108 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -131,188 +144,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -321,478 +342,503 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg index 1be6be46f002..29a9ad0368b4 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.893155 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,384 +284,403 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + - - + - - + - - - + - - + - + + - - - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg index c6864997c697..90b5fab01765 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.095376 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,114 +284,115 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - + - + - + - + @@ -383,17 +400,17 @@ z - + - + - + @@ -401,362 +418,382 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg index ebb92a50afb4..b8dbf48bce65 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.255825 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,451 +284,475 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - +M 3366 4563 +L 3366 3988 +Q 3128 4100 2886 4159 +Q 2644 4219 2406 4219 +Q 1781 4219 1451 3797 +Q 1122 3375 1075 2522 +Q 1259 2794 1537 2939 +Q 1816 3084 2150 3084 +Q 2853 3084 3261 2657 +Q 3669 2231 3669 1497 +Q 3669 778 3244 343 +Q 2819 -91 2113 -91 +Q 1303 -91 875 529 +Q 447 1150 447 2328 +Q 447 3434 972 4092 +Q 1497 4750 2381 4750 +Q 2619 4750 2861 4703 +Q 3103 4656 3366 4563 +z +" transform="scale(0.015625)"/> + + - - - + + + - + - + - + - - - + + + - + - + - + - - - + + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - - + + + - - + + + + + + + + - - + - - + - - - + - - + - + + - - - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg index a4e63b10de3b..d9b33747f360 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.736237 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 5"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,429 +284,451 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + + - + - - - + - - - - - + + + - + + - - - - + - - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg index c6fb8e815924..e7ba87cd63c7 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:25.870012 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,109 +35,111 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -134,188 +147,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -324,437 +345,459 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg index 8d804f5b85fe..a467edef0196 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.604187 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,255 +35,261 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + @@ -281,27 +298,27 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + @@ -309,17 +326,17 @@ L -4 0 - + - + - + @@ -327,396 +344,418 @@ L -4 0 - + - + - + - + - + - + - + - + - + - + - + - + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - + - - + - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg index 856034c6ffbb..0f7bde1e09d8 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.771522 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" 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"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,474 +284,498 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - + - - + - + - - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_image/downsampling.png b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png new file mode 100644 index 000000000000..4e68e52d787b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png b/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png new file mode 100644 index 000000000000..eb8b3ce13013 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf deleted file mode 100644 index 4c0ecd558f42..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_interps.png b/lib/matplotlib/tests/baseline_images/test_image/image_interps.png deleted file mode 100644 index 125d5e7b21b3..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_interps.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_interps.svg b/lib/matplotlib/tests/baseline_images/test_image/image_interps.svg deleted file mode 100644 index cee45754af4f..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_image/image_interps.svg +++ /dev/null @@ -1,1132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf b/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf deleted file mode 100644 index 7e53255f2ebe..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow.png b/lib/matplotlib/tests/baseline_images/test_image/imshow.png deleted file mode 100644 index 34355b932da2..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow.svg b/lib/matplotlib/tests/baseline_images/test_image/imshow.svg deleted file mode 100644 index 2a67f11627a1..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_image/imshow.svg +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 72918a27fbc1..9e68784cff4f 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 8123e200c27a..c0385c18467c 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 @@ - 2023-04-16T19:34:05.748213 + 2024-04-23T11:45:45.434641 image/svg+xml - Matplotlib v3.8.0.dev855+gc9636b5044.d20230417, https://matplotlib.org/ + Matplotlib v3.9.0.dev1543+gdd88cca65b.d20240423, https://matplotlib.org/ @@ -29,167 +29,167 @@ z " style="fill: #ffffff"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAMr0lEQVR4nM2cf4xeVZnHP8+5Z+adTtuhsBTbTpm2g5ZSUMuuGERFi6vFIDFqzQb3D7OJwSYoasHWGlCz/sBSaRMwpuvqriQbNLvZNf4GhdakBSEaQCqISjt2Om1pK79qZ+aduT/O/nF/33vOO+8gfd8+yZ33vec+55znPOf7fM9z7z3vyNihRYZEPBGqomoliS4WXUt9l27cdr11mw0uXeVo1xOL7uJnrLqtRPuFE98YqzM8S1lks8tRP3I5R0ytLDIu59R1XY6MTF1XHRm26tocCbEztV9oRwEhcMHQs1nZv+57D3f/7nIAvn35d3j7inwGlt99Gwc//Blr4wBDO7dxcMOm7HzfwUHe/8hHETFcv/pBbr7459m142OL44FZHHnu0iPOPk6HTB8ZJjwyjG6aHBe22VEY+uZMA+ARla7pPr+mX5K+sHTaNB79fdP4oYeSqHLNJH3Ubei0TJkAAPnVn5eVrHEZVywfNz14GPqVz0IVcK43h8aSkbzxIyt4IWpyIlSMGx3rSmBv1xJaVRtWFpDcCTmaoFhPRA2A0kx+5c/XMPbiAq48fz873/BfWfnooUVc/Y1NDN7+CM1r/oE9P9yETRpLRlgELAIuv+5rDPzvo4xtfAM/veF2lp+fD/Smxz7IA2MrGT77OW5Z+hMrB3UDSSmKddP0xCUFG/Y/uxAOzeGJ/iW1ikvve4lfhN+DHwLYnVOUR757EwDrfnwL3FC+9vuTi3jpmbN5YkmD5qAu2VANu07KeBSTtB5PkFOU1yw+ztG5A1y44DjR0Vdny+DBoJ+R9w+wrvlZTrzpXPj2zB2t2bCdV+19ngP/dA4nwl6WF65dNPAsf1k1l6XzX2Tc9JYrmu7xz4TRAOjxqLd28d633wnAqcNDfGhkHb/6t+sB2Dh5H3+6JUYCT8Hyu7Zx8OM3OzsZ3rqd/ZsT/d/BN9a/jfXfugHEcNUlT3PXebuYd+n/APB/z6xBUUeL1wUEpROlm5UZK65I8wZH+c3WVYxu3gjArx9+uqTbyjEABzZvBG7Kzvc3z2P0I3Eo7tm6nXmbv5NdmzYeaUZVRIzfBfA0o5hq9EQBOTYYN57L845jEwN/U6fPT8+1tguQLQwW9HRaJkwDRYSeCOuc8/ToYlYNHWXPyDAXHv4jp1ZtBWDNOWMlvaGd2xjd8GlnJ8vuvANuzM9Xzj3Gsru/CmK4cHiUx29YypplY7xweJDxk2U7qjlVJyXlYe2b+s3BRUNxRvqOf/4S9z90a+maFNBlNggtV6wby/pjZy1my4d/lp1f+Z6t7GETDMK2J9eVVijfEmKdktQn+lTYsOYXAA88dAtwq/Xay5HB88vJ3J4f546diHod909dIOQUOZNhmZC7lV9MFOxwTVan5FTYB4CeDHu6akgqE5aUolu8k9qifeN6YtNZCaIy9ymJiKwPS06/+IktunmGIMeG4G6FV4acieDMiPXxhHNspNxpSSdKTYWa7AjiI5Wrl36iVOmXI6/+mzr9jz9cUTp/15rPZd+bQU98hLp2dFomwx4mwx50EOWckyLnjqfeyerGYR4cX8mKfV+m78k5AMx/+jim8KTu4s3beWrrp5ydvO5jOzBfz/XXjm1k6xfWIwEEl4xz3erf8Pn5wzw5Ncj3j10a2+B4pNpJSflPT4d10tu4+n4Arub3/PLaN7P3R7EDhr55e0mvlWMAnvj6p4CN2fno8XM48IW4zluu3cYXf/QDAK5ghO/tuqxUt5vhlVKNbvoa1WKyphYUVjPvlTPYn1deJSf9HivndYMH01DWxghhpf9bn3gvF/Ud4YEXV3PstyGvG9gBwPTZkyW9NRu28/jOjbjkjR+6A+7Jz+f0T/HaG3eAwNFLQj5283WsW7CPPzSXYMYgrLx5ENxvI06nhEl6o6cDjVRm57s/vZLG88L4+SHXXvEod1zzMADvfPIDXHXVV9i9awvrzrmex3beRPGRRE3ugXcPfZJ7R3ewdu1tLJjzAnvvjJ356cfX898Pv5EHRi/DP8sw/+LnrAiu2tYJSRclFQQK3/eyIwg8Bg7Awsem6D/i8fq5h2gsGaGxZITICLt3bQHgvue/2VZH947GqNu9e0sJBZf2H6TvmGbh4z7zD4AfekwF+TGdHFN+51erZqBpBhoVBh7FI/A9mn8nnFzRy/RZhodeypfv8eke1q3+7MvqcN2qLYxP9RIdjdvbe3IlQb/h5HLN5HlCEHjlI1TZ0WnxAw8/8JAV93y5jFsHjIuI93T8PioKFcGUxjQ9ZDrXMI0I6QvRjQCt4/uj0DFIV9gUX2b+cf3nZxrPKyoX/yB+EqGjoBrowt8Pj3LBvL/wyInlHP31YhY+Gg/w8NUR+959FwOD8UOvt7zvdh78vvth19q1t7Frd460Zf/5VRbdr5EQTlwqDF92iLcufIb9EwvZe+CCWv1u8A1AECR5jvELM1rwU7+K33IqX+j9a4wUaXqZYwAaz7V+46lfmioX+ELjhRAEdDNO0RvKp1cFhEFsR9kh3UkIg9SWoX/fapx2uGbOquvoydKG4/2/vT+HrhNV1hWv/f6KqppQ7FdaWeZo2K4utfKsZq2Nuq6rbeOygboz3Lru/kQMWmqcYzemXlYg4FnMuKsNu+5M7don1u78ViColxmYpXMAW8JqhW2bCGhV7kyO2+7PNbb2QlK7HhlbDRMXOuvK7oE50GArdpGF1ZGz4EfnzJdPtfhi1Zfal6Ihtv4q6ZKzvrwCiKjbIKniyw7xeplW5f1FbRluH3iBg9qt7zCwHdS1cr6zjVmGuq4+4Hfyh2XEmQGVT8GuX6tv07OE7oFNLW5uT4Ms3/E1ALSqEvIsZjLNNYxY6jlWztrpbJDTIUl9oh270WKx01EJ00ZaQdzdZlWKDsnb687tgyRUo1XqnFks3aWGZrjQLvnaUdQdCKWAiZfyFpxiS2LLSkmxyeuketb6rnaYJYmeRpFkk3WOnESs8K6clHQMJcemCDCSkHuLwdWX49b6nRLlA2LhnHbym3bQ78rf7Nwys24nJeMc56aK6r2GI8Rqy3lWgbaWaWtdV38dktQnWgVuC4oDz+w3eZkRQQSMio+Uu9JQk6isX/p09VVU6xJyUqrRKnleZb2hzP44RAxGcscYEST9UUbBSXljLXKmmfrqoGSrVStCLtew66hS+JhMN00MrYiwnrQo67CkgNESFfb4ieRxnhpZeSRSTPokAhUYlG9QYaIrEGkh7Ik/8Qr8kaCputyDJRXooqToryDHcjtgCoar9BAwsWN006AnQnQzhNCAJwR9HsFcj6APQmJewoAKTZmzlNSck1nSRSdlyLERcuwcAWNKvGEUGE+IvLjc8w16MqLnrz7eqSkkiDBaIfMaGCVEnko4SZAoRle6EsRONrlz0mTUZkuHRZUy5KoI2VRKBBIZTJK4GJWUmTgfkCBC+REyFUAYIqGHavSgwtgZkQEik7QTt5V3UvhwOKc7GXL8qZVl/3xpWU6WY8FgvNgpUfKpAhOHShRB8aeDUYQEBgkNyq8ix2R9GMUZipyUcyrOydL+rKDAORGYEMRLUBAkHKIUpq8HCQ3GE4ynEBMTdX6PZWKkJSgUMfkkJP0AXUFKVVKfaOXX46q2/CYrjCiJn9Umzzskigcf9XqIJ3EMKSHSKnZAaFBTlEIUEhRWJ6Eq3STknHOqyBFr2h4Tc7zpLX2YLSYOmbCR/+IlvpAv3xIaa2jkKYPdC91crVKfaKkgp5ykFbO95DPKz41IlhkXtzNLwlUS2lDpeuZRN7JbDsrDajq5BXXsfasPJhdJl2lPMMWRJIjJUgEKxNsqls6M/eI5IZeQYzGu5WAkSeKUZCgCEhI3yTqeqBac38rhZwYhxz7REhWck36tLlfpN0tICICyPeOplEQmKyv9LMnljFa7OE+zSI6c0KFRNy4m43J5fheeckeCohSFNpQUymxtZtKlMJMgQQ6+4/WDw2BrmFX+d4SI5DM/g3Na9dUt56gEMFqCAnJs77ztWzxbt25MOV6cg0/DzNF2l37JKEGEEbEgpxo26ZeZnDSTw1z5jItbZmrvNIr4IQJo49e3rllDx1amVH6ttmPIum2iXgQzO77TkgBGM504p7jUFhWd20DSfygT17U51CQOql1zhtmZkeikVCPvGvgX+6YIK1JaOapdXRtKHE6xtOHeszMLh7cZGdo0p6xGmFaGtNuhQ9fd7syOy2ayA87XJvBnVdnZmUPXGm6u8GnFa22067RjFuMr5lw6XnbtiaCx3k4XuKZY7NA11pfw9TLnYB3EbuUCpaj+BChu1zI+JfViURQZV+y9t5Z/lA/WC1sSd7Xo9CDSSeht6lbb/H9/z+MBZz3wrgAAAABJRU5ErkJggg==" id="image766171a396" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAINUlEQVR4nNVcz68sRRX+TnXduQ94EjV5BEMkioZEIcSww4QNC5Ymhv+EhSsNca/+A/4BLk3eSuPCBXtjDCwwQRB4LCRgQN/cmempOiy6e7q7+jvdVXPnvTf3JC9vpvqr75z66tTp6h9z5YOPn1YQc6wRQGW1ixRwECzpb2INZh4DxzrCW8kY6wOVBiB9500JkTFggGCL4ogGLRFCDCwTTcdYXxsqRDZYTtngCU0wOFj2sf4A4AhHNGJ2QrBqYMlspJnnNzoNtYKiziZs2urkUGWkggMm3JUANRGBTUQFoLYGlnAwXw2HIM0+JzIZg1/HCzgz9fggR22Dj2zmbJGmHBXpb3FMhGuFqUiiTEVusdOmlqMh8Ru9mKx3S6yKrHVbqAUsa18Q2uJgQgPXELsV2t/X1WyQTUcyUFNANtAjxM6YsAcldmd+E1f0gDX4g1C6jL3pYvv7iTiHVJwU2CnZwUFGMb6JYvt1vBynXIYoneM71Ve4U/0Pd9wWdyqPx2SFK93hs7DHF3GFz8I38Hm4nc17jNhv/PDvdGBLdvf9FxfFpgXZPMO0AT0hW/z89r/xrWfuTTAXAJ4E8IP2+//vPYs/3v8O7sfLlntmlrMygg+o1NJywjLNr8PlLACYzugLl/eoMMxuP/MRXvhQ8fb6+UVegIvXYE8jSmddOZkT22/V998mM8fT+bXn/lkUyMvf+xh/fufFnpeKMp+tOdgS23Rn6Znl7tdherbimzl7o5hj67gyxWbGsHP4Y+IZ8RJ//qoVx6rY1v6g1DbxIgkm399IkOFZ5hoZtA6Xs0I7ifDb6M2DzKxd55JdhYuW19qMsbOXtdSuP2Fb9YtlxHdB9467IPsrj2MFGQVDJsHitTJi7hqw1IblxBLb72J6Vd5/P9WSAvrMAcpEsbPnekJdhRXlGPrzmyRzgAczc5twYQ/0KFGse5J5lmYyG5vf7nuQkIDGQTYBvf3Bc3j1+//KDuT9j57G7sPpslqaOd5+mszua6AtNllWy0H+4fNX8Cryxfn9Fz/FJnjCOz/QkkwrtX7cdgx+F8biWHd9hx3f++opvPWPn+HXL91dDOI3776Ov/3nu6gLJsEaPMvsY21YTix/frf3s05Z8HV0+NOnP8Jf//I8nrzc4JurNb69WuNxt8M6rvBl/Rj+u30cX+5uof6kQBQjjuXlXm7bmXF33PLS3V9SBOtok+X1Hzo+GkuRD0ZsXw+WlRUkFaogSJPjjMUGAB+C9bClbEDdU42Qhb2e2CW8DUcJdrDPCXv2IKwD9KzW8zkuYAn2fMU+ZI75cNIKiLVZRfKGid0d8xqaXhOYTD6MyHQRa3nNn+VHLbbXdFnRlGg6aXKQOjlg0/apAynCEl8Df7R72l4gNgB4hJl6TYIcki1lD/s67n/eYnNxFt+wGADM2Zhruxlie9kLASwEA0CNVCzhoIBzELs1L8ayMt7cODgWLIhqtd8IsRvzsueAtK8y5QtEuYliN8tqbiaIWEsDXeo/5jgzsQftnt7cW+g0EsrqWMJxpmJ7t58HmE4Wg1zob3AUiW0cPoXYQFKQJ4Cj0jkHa7c9UrGTdrMgZzkWAjCwFvc5i+1dWHBsGanjpw3+EYrdAr2EUzjmXYoELyrUx8RQLvaoIJsBHZmi1qqb7W/4OyazsznMfU6wcUWZYmBPJfapMztH7GlBtgIqGXxpQNfw9+5v3zRI5+3Hv/gdjWFo3u21KJiUjB3W9HL4YYudYV05meOYLKu5YHhDa6NLGe2dJviywRvgmYnJNXYiSmObFOTSGRUFoP3/kAarrv885C2pHZLedbmOGqnb4biN2Lwzf1M0nrnJklBAVCGxmQUX9CBOrATRA+rkINKY14qYB1m0S840t9fFRJhmDjJnTgGJjZNqp3C1QqJCnSBeCMJlI1Csxv1UJHupKhPrRNkzKSdkC9IXZCsYAPR+oypcANxO4a8C3DZAgkIrQbhVAVoBt1pSMQR/BKJ0dkiK3IJszygRsM0aV0e4XUS1DcA+At4BIlDvoHXTVV060GlESmbOCv46Z6nOrHIyLCXepb/AsgJy40NNrdHmX1RAFaIK1fbzPsJVjSoa01TIyVYWuHmo2KxbNcPMPiyrxSJJTvkSm7OUCqBdxlQCiDRnrwi4WulPL1OxU38jbDub2duKDHN7nRdbBF72kQPI0/dUQOnOUJUgwkGqVqj253JNRgkQp3VmJLZZezpRlutUqUlYElvHBXl0MAxT35g5NKdzFWmEieh/8qbd7LRPkzLE7kmnopyizgztUE5mC/J+5g1RNnPsjZWEQlQPbYf6liN2SU1i75UUmFj7uwGtd0ScvmKTrJrsD5IgOzp6vzZTbGQIOLN5zTFXJ+MmYnuph+dyEpBxfUPTfOCAXyORTaUhdroB5bvk47NncsFNxJbXf/Kr+RdaEqMXg9bLYQx7pNiLMQBGUSzwl/7ofpQ5s2QkqzpRlpbaiXkfVmZ77K17FmPH4+Dbbw9ZlAfFO61rTYuXmlx5Wo6ZKPZrUrNtN0Fsj6E41kDb4OUEogztoYpdwtuax57dRJ7p6IzqWyDKTRAbALzW9Sygbyc5apxNpEio8xXb667mg0kcj8mMAbXY8RtVZyB26ZJq4/Cok79GUzKTBpYOxtruM6ET7CG+RaGBg9gnENrrsOaIAwK51iKORAQIZBvgyA90RKZnjBabXpiJiZ0ThlwK0PeOJjd+7VUAwOtwgNbb32RHpRax8aeo6IwRbMPLsFaWEn/moxKWpfY1mh8FqIEGptEIzJHpMbD8b40ZdwTYcjWwSrD23zDLjxcAvgZGeZtNonkn0gAAAABJRU5ErkJggg==" id="image680ce2187c" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAGlElEQVR4nO2cvY4kNRDH/3bX7LKLFgkCxDMgIfEEoIuAJ0FCQkK6nBAiIkTEaxAQwDOQIBESIBEQcEKnm9mdbrcv6C9/VPljdmZuGl0lu+v+u6r8c9nt6ZlZ9edf71mMphFbw7QBQKNU1Mb1H3wwWqa/pB18x965HCStFvw2SspaHs9rA0Ct5S9M1FqnrXHgt9ZGWtfcimuxaOfZtn5gNp432/2iHX24ufta4/hNV1xnZS29sOR3RExLT23BpUbFWq7/4GM064JipU6KjtYVuFDEpTXZMvgZYJCitMRp229GQR9dlAbaMLA0AyrlQ8MWwZZ8cLAHH1ysWRz4jZtd2HRvN5GAAzU4SwCMZuN8sLXb5g30cbDphb1KJjl0ZAYqAuQGui7Yk9F9f8VekAY/g7J57dph04sAzlyK4X7ABJ4DRFpmo14hbNr216OgHEojQOGTXBdsdwzshizeYcTkuYGuHzZtzXVSIAWpAVinlXI4L2wAoAf3EBg5yxP++oOfYq+jffv7Z2z7qWB/9f6vYi4A8MMfHzt+87Bpa+K7FXfbk2Y0Zdtgsy+BndJKemlDjfO5rlpqtHPgcEHk80E+oQl8DWxW6+blbZ7yGYWz+/HVAOvXy2Fop4eekoLQpFMnZ5zvGtiS9tDKnipZ3pOCc87O+DQXKMtLPQlIbuZyVVnrt6ayOZMLgfdB+z58nLX8Lc5cYTnvzObRsOXqqd8Dd8H+moNN90HlAHUzl7IB/GGw81DysEMbJqt8bPTQLaWmmI6+s/xAXZvAH2OZLO11ObjG74Ex7MnUR788ZSPUbIbS4POwM35PuCeVxKO98YkJD+eqkn80lINhp6uqFjbtx2XFBZYcVmlZ5XlhAzyA3DioM/wbEFxH2VlZfzfwwVpWeRrY1DrLSkqSBVWRpOjjgmEDABmhcqQgMsDhpynSPg52jd/BR43WOeeYjoEzCxavwrsXAsAa7eXCnitHGhCkhLg26dyxUthkzdArkqnoF8+ZzWrZuCxsUfqKYZMNlxVbEkMnG1xkg8zasD0OoKq0TCwnHts9bK+ADQAEk9ivmSRdZ7nq4f70+182bB5O6v4WCsTZSLWtAzapTjGCTDIArFCKNT5YwSXAHo2UsKxspuwUMlCl9lXAHoxUxwvCvpYjXwFljbCXZZVJyL2cG2iuv+/jcmET+3CvBpTU8X8Am3SXFohBsklm+gs+qmALl48BGwg25EhwUDmXaOW2S4ENJDbkosCKEQhayfclwybtvBITgwsJFQWWgh+wd5wbNikTtR0QmO9SCzv08aphexuymNCBJXpM2FWga32I5xwj66oqRdAeC/bjllTcpQR2vCFLCdUMvjahC4VNurNJQd3gBfFKYUfLKhlACGJnrS3WlsQ7GWxhXwy10YZ8CeV8EbDhLis28NIozpIFtLFQZvjZNwq2AXpSQ+KnOhIw2t9+fCo4HuzDz79jfUixvENgKCiZOd0BtOtBW4Nm16G7JZibBu2tRh9/ugVWqaLq8fMo16ZsWiWlsL0NmU0GAPe8cdI2+x6b5x02z3bQ/z5H884d2rdvYBtCN34Lzk3GA34UKCp12R+GKfcLpE7IXoMMsLm3oP8eoP95hp///h6fdl+CtEZ300Bt4iP70CDDLklehJ2x3J158L1cIM19VY9LSAuXrIW9bmDv3sQn734Be/cW7HUDZZcyjv2WVCuXuKQtqZsxnwrY87LKbpImbpusJw119wZ0o9HfXqHfNIC10K2VZ0iCnbgxlFR2yrTxdTnYpLqeFzDvvnMAlQX6qwZoFMzNBpY07Ph1OWUSD5cSsJd4E5SC5VDwVd6lEMpgqydPvhGfB4VJRlZzmy6EPfit0Ra2QRhHckPuEp8Q5WZOmKHsbBhbri3Zkw6ArRi/sXb5lTQDZyHMlHN4LqoKvC7YpFr3Xs4EEZYUC0DzSbKwJ2EBbDGeoOWA18CeTF5WIhQmyJSMONASv0yjM5vu5STsMI8DYE/mVw4jCBOqgXIqv+eCTWg7OYj0wnMKcmYop/IrwSbVMa88pcAcFPljUsm2NcBeKicReN5ojwDFtbPCrvE7GqHjHiInOmrh3lsBZQ2wAYBs2yYFSztTo9wHfAGoKlCXC5vsvh2vpwP7zoQBjVr/E1XrhU0YKyc6AnFBKhI6GWwpDw62EE/MLVxW/d5fVuqA5LnAq4cNgND7pK17YJ47hQdFHTUrMfCkdRznlo9xl5oW/Aq3f62B4JvCStSmwb7+h2YJewkajykDtzVsdgAAAABJRU5ErkJggg==" id="imageb4935ef911" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAMXklEQVR4nOWca4wbVxXHf/fO2GN7H908NsnmsU2TlDZqSnkUyqMKQrSoID4g8RCP9EN5SIBAqAEJAUIIVZRC2/AFIajEBwRISCBQVT6UVghQC6IVDSqitGlCmvdru06yG48945l7+TAz9ox9x15vs3YKR7J2PPfMOef+z+OeOzNeceL4Bk2KLCHoJNl1JubFwGu4Ppc3R7LZBjOvNMi1RA7vzCHj+TwSRzvAAbBymP/fgLM9nQhqUzMjLHVe6y5eAFcLGroN6ZgIqAgdX58SoNt+MOnLgqRaR2s3n+oyfCXJP7UNADs9qYQkqWBKHXraoiKjiU9bDq5qst+f4LGFG3lqbitVt8zqSp1bpo/wnsl/crPjUhZF5lWdmtIZoLP6upXlRe8wqK59AOyatrsGLboyjUbCp2DaDilvPIp7YiP/bmzmkf/sQjwzSeWsZm79Kn570ySzr53n9c6LFDYepnJyM3Na01DRlCsiyOoT3fry7BgGuToEQPzh8GuMFkihus5ZcajXlIOrHQBuds4wu+VMF+/J4xs40JykpouMCZ+K9GIZ3eryQJAxaK+/+kTfCV1OOnRsAwB2TRe7Bi10Jp0q0mOrfYmKEDzZWM99B++g+uw01o5LHPjgN40KNm05w/t+9wkWnlvD1K557rn+YW4tXcTVIceDAolei24nmGwYJrlxqbFrymkb00ENXQBgQtaZtceRM4eYf2E7C39dx44fHeDUR6/rqcR/Yi3X/vQQRz+5g3M7Jpjc9ByTwPzRjRwPVgNQEV6L3zJE6ygoqcN2QxW6BjtrwFw4yYvNs1SOb4Aa+DvrnLzzOhZ2+T2VuDfWObFnB42ddSyhOXdiBldrqkEJiNJTyhQgKbWjBCqJatuNI0eK7hVqTHpYKP6yeC33nbmDBbfEfTf9hsMf+3qLVfSIfb2nvTw98dI23vn3T3NVucG7Zg7wxspLTMg6if7IBjNQw6YkYOwkdTJeQ1ORHhvsC9SUw1NzWxE/X8vsgUVu+M25ZSl8e0ky80CRi9uu4o97NLt3vMCY8DmiHEw2AMicerTSlCw2ths6mYF0ONeUg6scLtRLrH05QP5n+auGnDlEYeMZxsqbmHPL1JQDMqprNdVhw4hASaimHCwUttfZ58Tec1WRajAGwJ07nubqH7zMmPT4SfVt/PJnN1N+voS7rYn+VE5nB1zzi2/jPF/G21lnz41Ps3f8cWrK4XhzDQe8mQxv/nI+fKBaaeWG3Uu5FJqT9Sn2n9pM2fH5x/vubY3tvfcujn5tb4r7K7lKjnz8a63jRz63j3t++HDr++7Hv0zVLfPWjUfYUjrfbcMIi05SB+16Cpy0l05cmoJnJ7k4mTVy+wMvMNjeNqJnf7gX+FLr+7ED6ymdszgysciaQi1lwwgrcUzJbsD2VPf2AaDqlpl6UeGuz24zf1996LIYMHbcYuKoonpLBVcVjXVmVEAlddiuh9k+J4meMJTIQCPClTFAhCBDjR9Y+B0OGmVKQVRvAexGGBnW9lLcHVqKha0W/lVZQ28v7eHxxs9fsQH1aQ1YlAoBiYNMoIwiepJssju9BpGR4yWPU7s8pJ0N9xN3vxG+M7jCN+15EFKYWtsusbi+yLTj4YUGG0ZYexJn2V7QNkykDJooelx39Rmk0Lz/ic+yqlinbDVZf/EEta33Uz5uU98UwGfzlcz+6H4qx2zcbU2mtp/i7r0fpq6KVP0K1wfnWincSIFzJRRkvx055ttKjhUwU1nAC23+9vfr2Pmdl3j09A+491/v5avv2tfi67l9+Ey7B/rVwTfw4Wuf4Y6Zz/P8V6/hnbf8i7LV5FR9kkZYMIIyKqBakeOHWXCS6Uih8UI78qqtCWfWctu5j/Byc3xZCufDcW4v7UFt2A62bhngK5tmh4NGXZAbQQJOnFaiw0t+aOEFNlJonHUuB++cwP7Am1Hnl7eFeHTuBg5/cwfK0Tjrasw1IpDTaX0lpBRAkk12EJrvxguhqfsFClbI9OQl1r3pNKuKdZ46Pctrv7iPmT9VOfeWVegf528fbvrCPjY8eYEzt07B+Srvvn0/Vb/CXGOcRc/BDyyKdtjlmIRGBVayQMhmaJH+BEoSKBkdBxYNv4AfWqwq1lldrNFoFJje7/LYs/ew/sn5nkrWP7XIY//4FtP7azT8AmuKl9hUvgBAzSvGemTU66Q+QRh9/MDGD8xN6kqSF9h4gY0d5kQOtFPN9QscvDiNY61CCDjztgq7x77LyevXwnP5Sk6+Y4Lda77H6V1jwCLPnJ/FC22qbrkluxnXvLz4G0X0BCrCxA4DAzgtgyKTg0By2mt30vXXuRzdJbFslxsf/gY7p8/ylqnDTNuLzAUTPH3hGl6YX4dXq3HsdRLL8rGAwy+v6VaVM/nkcdcKNeg9yQ/impOOHOMDydh4FYMobcVYxWO85OF6RS4emeLIr69i8c8VHj32fe6YvZvqrZu58FbN1NYLjDs+l7wil1wHleSyFZp1kQ/WMKkVOTpsW5kxS3QdoEOBFgLXdQiVJAgk2lFc3G4TlLbw5tqDnB3fQm2jQDtNGn6BQEkajQIqaRmERqnuaDX6ZURABUnk6M606hE9EEWQCiDwLIQAUQppbFE0ZmSbz9YIW9Gope4VGcBOT76fY4ZJQYyJmH3ouzrXhtzzBo8OIqNPnVkKr0lubqQZec2saX02ynRlzoUmhgEMyo2IWMaSoqcHgJ1e7gV2l9UGfbYIhIGhjzGA7ljRliPDyHAlgB3LsEVoHtV9wk7QB9S8868KsKMvdscLDy2Gzmu1CfkBQHk1gt1Oqz4GpYf7TbTf9VkZVy7YtvGx0CBA5V34PwC2LYPeDLlK+hrZ5/ocGSawD3/lSwyTrtn3AFqQLchdc1hWOC+FN//cksBeYRJhlJTdBRmWaXw3w+UGe1iUPI6yZWrbO5CXBqodS7s+K2MUsMSaE3DSD+06lnkj9QKwsyNdMbBXmGS8gmcKMvQvqMLA17moaBEx9guCfpE2qthJSk1Uc3LqRC/ju4ZSAIkYnEEnP9BKt4JkTKv2aOaP4UubMhNqhZX5koHAGl3JQbYKcpB2eTdj37yXAiRoGfEKDUIRAZRON1O0LUffEKh35EBvb6bfrbQ0Kpl5alyEmkz33SvNcvTp3JsuK0utmtNZkAf2pgZLaXS4jMjpo6/Xo+aVJNkGJ6dA0PacKR2SCcgAZKCjPNUahEDZoGzRSjXIWdEMcnNMGSqJMDI20wQm1NNzIq4vlkCo6DdJxYWQwoKP9ENU0aI5WcSftAhKUT1CRy8qteWLwYv9EKk7cmLSneEvROZ3UghBWEjSR2N5Cme+gXW6iq65WGMVxMbVhE6ZwIkf2KmoyAlFHHX50ZrY0GN4xUkGkQ1ShLHh8SdKk2g5k2E7ZZLzIoy+CxUVXOlrpOujLi7w++pDqPMXsGp+dD4uyjLUsZz4b4fMSK7BhqC9rA6TEhts2Vzik4SYrfXbNS3iyesoTZwit5f2IEpjccppZFPHKUULUC1EvM3QiRizylH2OUFSc4J0LTBwdjZ2ilY0oKP0UuNFrOnVWJMTaKeAqsQ/F2qmoiyMZCSPT7TMiu/SN0JqNYEiaDcjGbuk2Uot4tqR+iFoUCmgClZUmyyBsuJHx55uL1Ody3qY/W7WNRqkjJEDtA3tWF2yw6kxCaogCB07GtTtYi2aqS7QAHb+diJpIUbU5zQTcJpq6UZK6HyXWlsiOiVEu34ojQiietSWldSYTqDT+lpS2/JHsvGMwRGhMmwQDfsEgfF9EKEEIhToQLebOKVbkSd0aoJCdEdDzutBuQ3oEKiVVqKZvttlMCgn7zP3cyC3RmVlGFK4E/ARpxSAaIET5PxkJxcUs0eF7kgbiRnsHNm56dML9BUiGdfKbOTA0kFJp0MKX5GMdb6gMAjYadmjaALjgLFpxhuJ/Fetoj/pc1L0NjoNzHJAGcW7bimSCTgiMFXZnAklIR724e0DNOT0VP3kDomSbOofOamcbzVuywQkTTpdS3rJHQW10iowPNXLBcr05ukAoJiAzuMdaeREmNi62ewYyZuspKsY5KwkYpDJDgL4kCgBR7x78q7sC1BLSK82c04HZ+TtBfoSrs+zbaWiHLC1n/2XCzrPCJOyPME5vGa5SwdN5/K+MsflBUQbnJSw1jtRAxi+FG9nQnQQzxp4L2eEd6RO+zB7DzSi28SHDAqWDtTlBjXLPzxQjeD0oqECBwOlweUG7r/WABWRd75orQAAAABJRU5ErkJggg==" id="image5eba289983" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAIlklEQVR4nN1cTYgcRRT+qrt2ZnezyaqJkL1EDVFICPEHBC+KYogIIgHxImrAeBER/0AQ1IMnERP14N8hRy+eNBfJQQS9xIigAeNPogQNSRDzx+5OZqZ/ykN3T3dVvVfdNbObzPggkFS/eu+rr756VdUzE3Hq740KhIVCWG0B5QggBOFL9Gd9mcg0Bto3IOKGgvFdOEG2myYT7okiOGMGDBC+JOUAMQYAKROWGJxgfCnSFON7ZovdmyBM/P7XRgoCACAkBsKrh4sxuQqUXWUPKyimvTL7oaClEBIS0dJWFFhPdsXXbiIHqcco14FGXiWGD9lyWUnG2Ri0oonIEtrtpK+qEG/6N4mRE03NuU0846uaq1x20rblFDDrOiRqQwhF1hcqBkcuFYMinIsRMBi81W7UWbmsWnYnEyhFCkugHcObbAqDL9kWhuZEARnhspu2yIfc4AdEqXrfSSdbLhvkDKRoBKMSDxJYvkT9mECyBzUn8CDl3tkTuHnTWQDATe+9g5MvvGz5bH57P44/8RGChRP459QCDnU21cYdhuxHtvxoPWtiB//YThNVwSX2HdtpZeZ3pazjc1u/LgOwpz1AVbbHj3+9J4/dfJZpRej5nrzlOza/yz47fkctBtlJ2rVO3Iz6WIgU5s5IxaXIy3xpZQ9rRTnhyggAyJ55ztEkRsv58cNP4ZdzG7F+dhnqPu5KAez+9hmcXprH5vlzuDU+VcYlSXGrtYmvj3WLXdoxMbKT2LsVfajLOnbSFj6960DlyfssgC/u/hAA8D2ADT88hvVTy3Zcdklxh0XubuVnHXMjIvLJyxVyqKptEnW2u24oMH8ubcDMfFQBU7N8KqYRUqPsptZJ2vzE5O2yl9LXB257u9ibGQrMmcW12Dz3r+MwVj8xde0+1lOSOBIY55zLyZSRuABZ3kCqO0QvocmssygJQU0EdcTnFMFN2DDW0VYMnU9288GWDiUpVKdhZ21Nu49iIrg7D0UKr57RiLqctGrLiOwTs+mauXYYDwVmvt0FlYvLV08Kd7duZqaKKaJkLy6dRK1SMkB3fvkqzl9ag/Z0hM+378DuLUetfodP3oDtP72Bbm8KG+aXMB3G6MbcEubyUe1uZTe1QsWuMiL7KT0DLpBHHnxr8G/2hHyjfkJ++Jtn0U0kSTaXj8WwAuecctw8BtlPdHK4I92oO8RiZL834uJyg6eUPax1KxsRl0/282XFJabA7z2yB/dfcwy/dRew45N3cfSDFy2f25/ejzeffwhbp0/jq0vb0DtnrvHmSqlf7v7Wi2VtXLHj4GtkFqojT2Cz/mbyoXxJz9UhW0bmsmoI3nf5TRrZACCThPugxG9AxbvppJHvaGT7xM1iNPO1CnISUx+cFU5lVO7zPJpAH9/xJVtTDjkoDhDVxhXJCSVbqqTMrLkK6y9asHpfMi9JNut6lcmWylxWDvUo46FLaRYkB9nNfIlclXxkd7Pdg2wAkIiFIxqTjhoU68uDGXeyJVKqlDMdKQd2Nlxtk0G2FLEgHLiOpSlGim5SOBszsvMYUiT0U1UjO4HhSOV8x4vs7B9SmK9nBN1XUcx7KY3zHV+yy2VVA6j6uG6gdf31GONLtiTfNvoQxXX8H5Atg9jtwCapBVnTn4nhRTbzeCXIBqAXZMthKDk38eXbxoVsAHxBbpIYcMt50smWQeUmxiZ3AFo98FefbCkSq22IxHSXSSdbK8gsoCElupJkexHtG4M/54AdkJdSGN+VInu0JWV3aUK2tqycgIYYfGNAI+T7ed9LTFC3bXtlP4mhajKIldPBybDIuiiR/6XwTaHvoaOSzcTwXmoVK8qJa3Jp5TBgqAYlAAQKqniLrQChVPb7DeJ64zd4xpnD5mHFuF1kWwXZd0aFqBAk8h2z+EPE9akd1kfNo7Bhpq2Om8FWLisSZNnILgkFBEmmFJFmvwJKQ0CFgvxFUBaXQ0xD8TolN7QgVrVlRDsEmg7czKlKfQkioLWYYGoxRtCLkbYlonUS/bkQSasoStlSy/qKRurRcdT7+ppVTohcekHOAdng9ZZCHUIBspeidaGPqTMXoBaXINfOQSxci6Q1jVRm2ar1RyP8KpBS2KCcOOJaBZkGVPlIJldCAAGRAkGkEC73oM5fxKFLB/BAtBfhulmE/RaSdggVZLMk0iopNqKqnIc+PHpYkNg3dHMDkEHEX+O1jkH5SAUCKlUQKpdnEAAz09gV7wFm5rJ/p9m6Vrly9LcFBtlUSmehHt2oVzVmGdGWlfOEWr2gBhjUEiiFZHYK4fXXQcyvhWpNIVnTglCZqgY1x3ipViWbyqf55jPatFY1sWLi6LhZixRx5YcQWm9e+iJR2tJIZiTSVgihFJQQSGX2LOg7Xi4ldpudryClvk75mkhcZOcfB5sFeeCQVBVFzVx5BFBhTogQgMqXW5w2JttGbpOyEnVGgxOV+DmTQZR6gTR/NKmEyHgK8j9pfuZRSj8h15INa+YyX2YM1PdKPEwQBdlMJEWSWonLqk3I2dzdhMjPPea9glpS9WSbGFgCucE1tCAyiiBBthRR9W0XAYi535BbLzObJNlFR+swRhAIbrMYXj3WzYBS9q7bXnd/ocUw8jLIfTmM8m1AdhnXAwNA1w+ffOY5R1OOMxihqoIUZvZXK66vssu4ZWP1MadsiSjmk3AXzyLJFSZlteJyZEsREzdPLjFFCv81KWdbLdk+cbE6ZJfKcQHKwYsVIKVq4062ROz4FQxZfJnq60HKpJAtVRSxDno7oVFmNxFeRI0v2VL1I8OPUksdYboveTYgQQWwCwKdj8TFxV0JwgFI1e9rDYoD4pOQ8WUHSH7FnPmfdeivozPY6Mlrik0nJw+mfariqRpXYk1RV5ls01f//uBg59MvQTvFo3QQdllQzf7g9FQe9cmzlvmUDYsczkjSPOTspUDgipLGxf0Pi5h3eoRACLcAAAAASUVORK5CYII=" id="imagef875872d3f" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAJXklEQVR4nNVcTY9cRxU9Va9m2vbEI7LIZGwSNBDIIpEQUoSyyjZkTf5JFsACdmyQgH/BHiEhwRKxAglkCdlWhIJBjhVj7IDHcU9/vFfF4n3Vx7nvVXVPULgru/rWqVunzr23+nVPq3v3Tx080+BWsTGlqK+MkfprAYP6CsgsDslXE9xKcV/TON8JsAlYa00MCKBxLhzrgpQxRv9+8zbC6MnyfXt/GyH3BLA4JF8b4WooNC6OuCXMbKFQdROsS3wo/4xEFiTQkUh8LRGMFjFUuiml0BDkCirZh4bmvkrRfWhoWNfArFyYMBUIQwC0P+56cO7LMBKSOxIqgaTIucNNhjoMlla+jcQE6dphSKltlvYgBFVEYgJhFTllXUSYA4NmpMuHhoHocT517QiL0zUZGsysXEgOnEQQEyAJWphPfYWxFqOEoDQGYAeSI0vJ8cC1RMicurx/7kz0IPk9iB4wCtXcmTm3V+S0EQLTtLoLm2CpV0A6nLAJkTQSG4lBwvDXMiubKmeQY4TJFh4WSHzJyUpkQ8CYCj7pSAUHRjAYUWblDtPZbkL6Hsg1vcZJ9QxvHD7D6SufDOOPPr6BO9sjfNq8gOd2kYVLT1E4cT+G979+i/rM2a8/ejMccCnBZmlTcsSipezA9kvmHN9ePMBrX3mY+J288glOAPz9/in+vD7Fo/q4w50gPEMJrS/vcKUWHBq4ysyyWSSDzNEn5pre4C2BGN/OXn0I3Ad+Ux9jaQ+nSY9jEBV2CcwAeB6JgnU2s3YmnTl0q3TCtWqNs8N/4RszxPR29upDnH30Ij5c30Sv0oQMoeiW+pZYUk5IyptlMzrJLW+cdL1a4b2ry6JA3nvtLv569xRx8acKLeicUsfKsf6gpog2FwE5eVI+uPm34mCs04jrW0nqSJ1nV+vLyRSuWVuSVp3tczKxXa8ucNGcdLjzCh1jkNJtv9Ra2kOhEXj3nItmlHpKRpUUKnYvyrHndgH/IKQ3rSytchVdYn0sUwdlNrYKhuYWf7I92imYB+sX0R8EI0aqNSyGy1A0y5h4LbNqZCWwgB+svoTf3/sa3vlqft35yz++jCePjtCvRTf8PybHr7UStlnXI4OKBhI9ZGosfvmft/AO8sn5xb/fxtPtVWya8LTYJvNqTDXpm2MXw0GlpaS3KK3kAPuxra1w9/wUP7n9HXz/zd/OBvHT2+/izpMbWDWG4FZFBVdS1y427lsm2myalBz2rMifvK4Nfvf4dTy+dR0/PvkTFjfv0QB+cOu7uPPkBp5urogBiOQQIpiydzW/nEikm00dSj0ntQDg8fIIf9ic4f3zU7z8x2d4/eghXjLP8Gn9Aj5cvox/Xhzj6eMr6MmfI3xu/DKJAdoDnsNV3/zVD4NXJWdpXJNdy775m6a+1LNMkbmHDwBm259sISklSmAY/w9km7oen56yh/AlpAkP8YvIKVFHSWyM9CkMrRyMbcij5WDCiFpGXonvPNHNpO/no25jrfDhbcGCYlFLxlWRutrxEt/LJdnYWow2OwDZlw1+HqSXpnSer3E1Sys+GcrBRS9KC7W+DDedoIp8+Vo0BDY2SXg4w6CRaBQGI3A36SsFEZqT0o36svUU3bTkC+SRnJIjKYE5iKcwNRav12K4LF+G28/PULQXb856RsU1R0yTdMgV+MqkS8pltU2YT2NjByepmYdglJRWEDbvLaoCFQkgBcGEG1LluJJDNtHhuFG1PEGBETQRtLhp5sturGUnm0dy57gDyW1aTUxkL+VudhqDXWDkOLJi6OJQ7JQzcUPlTD1UK7gCxQVxLoB9SacxCO1/V5UbXfMX5kDyUrAsOAX2SW9+SvT/LVW2hBEUZKmD9jZfoOd8OS6r60VESxhzDhO4CnFBngmC34YV3Yh8c06H6HyAXwILcKUYcg/OaO9LluLCM8Hsq5j45REjXURqnnx+7OuSKVN7NsojR6pnIohUN3YkeRZj7/qX1q+pPc8XZI5Z5FtEeOl6uTFIGNP3HM+vYBP71hQJo8S3lPSS9YAoraRF90236RYr/DdzI7d/9gEPbMbe+N7PZ9czuhH6vxDM1OVz7ta721VBWotj5BoVRbSI3MpRljqtdY8P/Nf3TjPuLMaWaVrYt3hDHleenxi4OwQ3LqdbjMR/h3as4jvznqQMMPG+SWxiWrETS2qPA5RzUA2gG7R/g6AUrAFspQaSQlwp2vS/xbfkAmP7jtejynEq88QcoGsHs3Ko1haqdnBGob6q0Sw0rOlUNOAm75cnVSpdVS7DmHJiaKPrzCs6U5J1qNYOB5/VMOdrqHUDt6igjxfYHBvAKTg9rsqfzqHwliuMF1oiCtatWNXmXSUstsq15Oitg1430MsN1HoL1xxALwz0poLTCq5yXv3ocdNIYjJKW3qp5ZQTo7eeEwsk+uQmaERNSxAcAK3hKg3odoKyDrp2sFDknpR2tZI2P0VyrvXKSQ9l5CNIq6KbMDB0KGc07LVD4NAApiVJOUA1DrqHzyBe8mtjY8Ht/rWUft/TN+R6fBSY+AmfvvupBdsW4eaKGbvVQa+eVkFiS49TeuKec9ktvV97qkGMymGLNW6+pQ/qUUGLUY0b/qJWEZLlghu+oEDIDQLZzfTWySrtFB0oJ1xYODHpj8YHZIwE9Ys3fur2uPF6PkDvC+4rfZ+kwBQryJGijRbIka7t8jMV5bnI+awkXEK6HMP+X4HTW7LviHSjtv7TLn+DoOO9iVLXKUa4SaE7jt8N8V6ebhb7dStSTiI1qXe/9aOs72iIp0g7Tv47yxySZ2MoIC631gGxciZAxfE9ySnBFVOygHTeHBTYh0IG24xHgew9Ub9I0o7zCaDvtaQfzygihuD22ARXEpNRdZ5yXMx4I/vmEqSANH1I7ZmNLWn/4IqWYpZ+rmFQjijvrvX6HF5C2jimvB1xg1f3xcVItnHbbedXWmtoUcjGYLkvYpSk1CXg9lwY1K1ywm86MaCYjO5oSDBFRDOSJV8Rw1PRnGIKcMOCLCw4AggbiXwHor+oJM+UkN6M22z2W0jwpRsTJc8xaIqIhBEMYT0aG1OO3XQ1pyTwjEUH9XwRiC5Rtodh0P2iiaMf7rGLAaDomyYN8m1ixD/808YkBMV+u0Ilb307X4lIFkMhkR0ZhgbUBYX4p2A6MBf9KJbSsm/6A1oKLlpzIMtGGFqnhE35AgH2iJvG0G0kjdnz/S/SrbEiXnbq5gAAAABJRU5ErkJggg==" id="imagede6ab4e840" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-54.994689" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAIAklEQVR4nO1cu5IcNRQ9UmsfxobAkb+BgOILqHLMh5BS5S+giiIjIeQziOEbCAgICUgIKXu9Xk93SwTTL7XO0WNmFooqK5q5c/vcq6NzrzQ9vWv++PNFwDQs0tERGwB0xlA7x0h9rbie+R5xU2SdQ+prBW5nWMbzNR+GHK4P6xvGlOeEYwyB2pnSGIYV13ulHJP6+6CUk/oqlXmSx6wy9za4FYCAIqhgihwSTBHJSGM5AOiIWZfVmBpDW3kDgLv3V+QCzxPcJh6EfcGoIG96KQkhGPH162va65J5h9Q3zLjpcG/DNQk6BZQkpfaEoAwGVWgWo0DSZlgyjyaVb167fiqrPnKYJhRiPrtpov2G5zlAj3gspOwx4BPfMkbZN6fePnDfeM7p9e61v80GAFZSth9boh7lq1eZqGqrtBoMqsw0B4WhqgMA3APpOYkMl7pMgRLwxZc1cZ5IRFLI+7LSKy5U4XqAk+Qepp4TAYT6YFb6EnKI7zGptB12cocjGIqcmt44Y5Dc3L2PycnKN5GpUIIJia9MtmWyDbgs36NvvaLd/XhDP1AXVW3zBf+T+kfiy3awBsXnFmAaiXIisF181tG/+ewnGgQAvv3ty7pERbxsAyW+X3/6i8wFAH74/eXky3IICa7rQ1rvsy0LUjEUdkLQ5Lcnvwchc/bd2fdbey6fHl1VY3Zvh7isVNm0yHged+Meux6D+srdrm7B7sZb8VVI9Jx345VM5mhnTbQumXfj9UXIZr4tCp7H/tjCz2+bnvPeuymYICGweyN1ib33LoOb+itchnEKOfNccxjbBXIH3y3mUuBaUtZkuggX4KrTpNSfaGvU/G68zmLs47n74Vp+uNgbj93zmLF1yT4OUWrMLaSEOw83+LhsqicSbFHag9+q8cx+EkQODWruSYtA0KozX/z8avmkpfGxpEyLQi6ipvMbeC6eO4zpWUTdG2sJehGi/uMFcIch7uAtwZW/9G3AZfbHIlxhu2G0Rad8Yi2+503431Z00pBLQC0J/t+Jdn0f9xx1M75loi0YEpfYWtTQsgDK3/m5rIqTNBt7SOx1hBhpq8HY/k4VYUz2GjVs97dSbs7vy6phJdXqoGHyWpEtvvmFHYu+HNf5YZ+FoS85UMmXRLw4+ZlYGYwa8l0YMj+XE4DQEIz7Gqn/dCI5X2a8LPEOo/45lCdmaBL8Z2sxOZkYW07uy+Ip9ajcSgTH5GR42gPTpNX1iV0QLH0VbowR1ISJL5Ajbfr6YJKes+aUt61vVKnlMSpWooibLmxyxYJRIYIdhjOkrMSTHVIthnZjHTyNJ1RIawf0RhkdJqRBq9UNOEOe1pj90txUD+HxKMn0tNqGW79R5EpS2DfDmd5I56YGp47ohGCtzHr7HsOoPtiIu7U5u1FObdJq0oBQvFJ2xaTXi2JkjmEKGJW2aURlpcspA0Imfk5ySw7MsWXxVB4N6oobsqiwliCBzeGEcqEYoq2I3+dEHpUNGoAzg/6QkiVqIjupGhsUsfWHQLW4beW7vqzrOQWQ+e25tc4x+Ix53zuOGqXXtJC455zI8NZ0DsHSdhFf7px5mBTObspKnblaZMm/r3Df878fNeSgMGp7Tm6L3YOXfC+BcW689CxU7wsAzg5h46CkQzBPLMFsrat4rWoibarFd+05fmsLGkgmaFKXC5O0xhIujxTP2c1TP3Q7LoLtbhtUEtu09eO4CI92fhK+9ITctkXyt6fIOBdvVvWpucUlVtc+nB3ZTaC2bc+MAXYEEI6P84QO8J1JJtJaQtRdkGAA/PrjKxHgOD7/6vvJt+7GV7SVr0nV3eULBrAD4B48ru5G2MHDO4v+4w7DrcX2WSFaFhncFt/aYQetMGaOdquqwDv5dAePm797XP31Bub+AeGjW9gXHyM8v97NcrsrioAn7oC1o3Sm2+NKcnQDjP3tIaC7H2Be38G/fgPbP0P3yRN0Tx1Cl/6yoVWRWi+toHmutbj0TuDRL7DHAVPUAITOALc3MN4DT26P7wGwx2cMGr7ly9XNN1Q15nx0DvFbZ3u9FRd3FAMYH+CvOozPn8E8fQJ/08HfdDD+qKpS8lULsOS27IVFXzZ0lXB/Z3sfOSV+4pf35dwQAH9tEbobmGcBwZijckJYajx7e2GvXFoykxL3G0Vj39mTs+KK3F6+/C7kApXOBLEvyyhPbozb4stzyJFb5btRsjMDawzrVctqiV+Ni18fRv7djR84+Y62vGwhGljmYRp2yq2SnT1s3pHgJdZTSfJdJ/FVuNlFqCurlt6UU7szfgMwvybKkYwnhIaE0KxK9iTNT9zvzAahmlBDckiTid9G/n7+ObjP3wqUTWtOKH74hftm1JQMm2JQJWydkqZukubdUnrz3GJydhfoJDdDPcHCH4A5CbdYjhWNvNy3UrtDT35+EAlQWP6UI01AKZPj1l1/EVwAhpFjhrxylmDqSUP2jyoaJmEugMtW3QCcCPXPOahyGDlAoggz6uSaVnRPxqh9a3GPRBBflXMlyS4cDrvrGgiwUqPSnjb285Sn7FLpGbL2nzgMQ+SUPvwjCLAG8PEB8iRi92fQFgVlCEi+9J5AbNqQZUBC0s53IVYmssUYZbwmkgGu4KaFIi0EpKxOCijKi06ygvyF5KbSyCi8Ji+C6/xB/9Ex3UkakggqkZZedWHi57xqiHfwYrcCEOixcqQEGXEEDYxM8X8q6ITHkRJED4705hD4M8cjxGawYnz4h2aZ8Q9uPDRhuYg21AAAAABJRU5ErkJggg==" id="image6c6c3130be" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAJH0lEQVR4nO1cPY8kSRF9kZXTc7fLng4OVkgIYcM5/AM+DJAw+BEIAwcMwMfDw8RA4o+AhBD/AOEgLIw7ARIft9zczs51TWZgdFd1VuSL7MydHgnj0pnprJcvIiPjI7OquuWv731ecWwBdZtIHwBMIlUfG3/gqLEAEAiHiyXsTAcPGxzeSTyt/fl80gDEpKcPCcBkDJyPf60Vk2rlVen4165ohtJVOHAQLPGIhFRjddEt1NhKB47Nmlyvijca1w8TFNAtINiOBStOPxTQ7TU33ASwAkPddeRl45dJpU0/4xgJ7aXFm7wzJHzSQfK2Q30s6w/MmA5H94K441nTKioAIHBRAIA4F54TJJs1AKajQyadTL/CmGs1YDKrNEGRtO4DgFxgFwOy8QCQC47FgBVWlMxhGW/0hfW5bVqJd3p1AqudLjBZjzECt8I8LOFl3qHEQxt4n6MTC+KlevLQ+GF+o5uIGco1SOckvfGMYyTkD/jORbEcR0i8zdddyk5SJ2smfBVUYfsNvuphOTyD64AXEw5WiAAgJrVlMFCSrLXAdKwhNlEmDZXh1lJqeJNO1HAJgehw0K3Wo9Z31c1wZw10PAhHvDFhxSsNs7YfDjbmPexYmHqVkXCM5C0STkuLH6U65/iltE+R7twwjPXCmCXgsVzIFiXOyk9PzECZbKSeSMKXdv/CV3b/wOcmxT+T4C/zc7y3fwe3Zg8FnapJz/CqDsPyEFyq3I++/Hs6l6X98s9fw6wT33OhNmi8y1dNwNKCKKwhJ8n46bu/3fQ9B/Au/gYA+NmfvrviThPZcgQoZsPJsIuhSuxBX6W8rK17usI25QLMZh8eX95vqxUbdOprbCdJ+yjV3CMh4K4wzVXndftverOJrRLyq3RFgS3lvIRn28t0TRXxxl/CcK12m7ZhTuUVusW5KOW2ImQVnnsacVu2pIIEIbwH97XcmeyQ89HVrbyk7YpJ9bGnd8JRzi3eGc9xvaX3MFm00iu9Uuy5OON2S3SnF728vx5KF/H2fkcvsEE9cV02xu2Hav/pfMRwZXt5DKte3jinbYaWEeVF8Z0//BBv717hC2++wJOwx23e4e93b+HF/glKj/UV6lvJTZ+Sfj1/U3POdUVjZ6qlydd/92Pdgvvdf8SQXv9YGPcn50t4aLy7j/QCuz82IvAihnOUZtyPsSDxbt4aJzh3DUcUGsIyJR9J1sgCAEBUc4cuqbPqKpXhlrt7FV6FlF4HS5ResUXfchewmiDp92RlotdyJGKGqxKyNwGr7NJGQopd+3/wVA8f9/voCvBuzHOsp3jfeJeXqzBkKD//tcdHzQeEMjVcBWrsoxnHsU72jKBEN05x1utjJmQrobm2DKzuSorC5q5lUhYrBLvgaw6tZTkcbPyKJX0Aqicids4xz43N08Cqe8tDV34gH/jyHpoGzmOjesahmtL70E1FqyuiNXlT0V4s76e6eVB7fEAuoOWoBeds27dYqSRuwmHDsZTZM7JQhAnB2oms8uzMVSrDrSOJvBK6NQ5tpfEGQkqcD0veopf7vASiVQFpeU7tvVyAzU9R9o7n0L4zWADKDDjIQZsQty3Gc2N74znWIqKkvtVyilolVFCHWJO728iDvB7o7OKdWtyc2AW1+TzOhfBotY3xSKmv+oVgN9FHPGyTujpyIetjq+zkxyjzyUWrYQNe1C7x2wG+F3b2HQQ6/f2e4ewd1n9juPeB3iT43oWDmcn85Dmig7cY9YDXXYy4as/ynRtKRLgXjky4HV/9c8KeC2fYjyREWC63WDY+BpuQmcCNcnxTxbF9fUPjW/0M+AAdYthvO1gepKV4pKpdIIzYolBdHyDPXo5SvvdlDO0q2hBMNqgulvWzS0qSulvZBw3e0jduFsSc+N1YZULPnQ/PxbrlYHqMcPTMg2xDZJNz5i12pJz3YluGfcj2wR1iOpoR0JhHZZw+6ZfZq4yE3CUS9hAHgFg+eOP7FziblX7s2H6JYy9hdLo1ahknHN/vH1tFGQqpriTtcIyG36XkASas+omU9J3hGM0lVHHPBR8oz8Gejg+twQ7BULg4+LGjRP/9pBHdPKNHmyNo2QSqMl9iRQ/XRRUqcnimLw6+g3fpr7Y2Z3T7469+QohO7as/+IUzDx4JcZpP1nmdcJAMXL3KuLpJCPuMvAuYn02Yn4T1xYeRcHhdz+3Zr8o9wTYT8mxdhwNdN83A9b9n7N7/D/TDG8izTyF88R3kaYccl0F+jnq07QNp097HMu7YfOeHrIwlCUkx3d1DP3iB33zwa3w7fR/TZ99CSFf8lRnjRR6vlT+KZU2yv0iC2sNjuC/Cik7GU/DwnyRAJ4E8fYpvpe9Bnj6DxgAoENKCLRm0JsOZiQMbo57FehTMETY6mEczVVg1Fdyg1j/5ekJ6/mmEt58h7SLy9YQwK9R+mYmFivdMscLqWCknbdrrWFiF2Y8r10jmCXy6CsifeWOtMDoJJCnk8D5Lk/dscnS8zjVUo5VR4skreeUb3/y5sgstggOWSefgnlA5j+3sw+A8Gk/DoxSew96eAEC/NMlXjrutHw4DIU0M/6ibUAAx7G1iaClTJ8X6Q1sqrzT9i2L1aOowjN1+jpJMzlnI7LdFRaps3l5lixWul6DCrnrYbycuG89qW08UCRzbMpa9EiX1Tpi9SeEtAZOvD85TQ+ew/ic3rodG2W9PnutYmiwH3J+9/TXAKx6vwzEUxu6CmE0g9uaehf9WDwkJz1h259jIeoZ3aAE8bic3Uu6Ctw4rm1uOMaF2gkr2LOtp1vRX+5tCxhleKeOh5Fh323YKR3xpPIdDmL5FPao9ZzY3dMLpWLBpA65MQ2cxSg9vQx5FB3kQr1csou73FfjEM2CQ4L0+RwT3hmMLO5J3zvB6um1zjiFpbtHtj/IEfgy5pIH7OEhy995eO8MbUf5+xebpZ0BVk0vB5alSmFsfJlq9dBakqrOrAZMhaRmxfof3yMH6nXkwjqIv6t3HXIFFiV5lSaNew3beQO2JDtbVw/t5KeZJnaEa88eOcYgwcZXlE+Pv5jCDDYTTyEIc9bBNOw0Z3a2kpkq5NZIq8lQbzsHq4RvyBnsAdxluCT2rm4tXwnvkYDoX+n7yg2aN9j8H/dv2rLJd0AAAAABJRU5ErkJggg==" id="image3a51447072" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAI70lEQVR4nO1cu44kSRU9NzK6e5jZgR0EiNcPAA4GPg9nVxj8BBLCAQPw8fAwMeBPQEKIH0AIZ4WFASuEBDtieqenurIiLkZVZsXj3MiIqWbW2TCmp26eOPfGjfuIzMpu+dvfP684DYd6TEQGAJNIJWPzjxw1FgAc4TCxhJ3ZYGGdwTuJZbW9no8HAB/0/CEAmAoHx9PP0otBtYqqcPpZ7miENjgMbMEREGqsLra5GlvZwLFRgxlV/lb9+mGCApoDXClYsGLIoYDm18x0E6BUuGILepbeZweETO7o/P7UXoa/jdcJQb1gJ7GSAcCkDedUHAaWcIxshmUDrZ2GDY6LAQB+TiInEEdMyj0bDUcGskNsERMUkWCZIycoYmGHg3JdokUcLfMrKBzKmMvLit/p1RmsxDlW5LAdVh5pE/o53ADWtqETCxKpyUf/Ij7qImFOMhcy4NARDnOBJVYHN8Sw19/FmxxoGst2g2N59PQ7HRiMYlq7Bpxu1C4ftGyD588p0bltxgQ7JdizgoUzdV5qUuq8oAlH4sDFjtyGs225HfWiF9vKTVk4yvmlLgDwt51pNVJLeM737+Sl0Wt2WFa3Umxx2X8YcufYrfQyI0awFv5Nb4afld89lU6adaqJdcJjt8dXH72Pb9x8gM9MT/Dv8BJ/un8bf73/Iu6SMxROespFz7C6zkSwU7NL/vgrf6BrWcav3vsmZp3sc1fhJL+LV03AMpwoSkdOEvGzr/0uk30OwLv4J97Fe/j5X76XYY8LyTkcFDMzVGKNlcix0ArLxk6v+F3Ayba5OIf7l4e8W5UTclnjOEnGh6HmHkkBirW6m5GeuT2PmtiqIL8KVxRoGWfVDjZehWsqtzguddzWuEvs6XG8nzVt3WXbk8rLca0d28Yt3DXviaPirh0XwfWFxkHPGukxZXkakR03itT0uyJyrEWzUNxyUBmVrBVbIT4StdZNZTleHm6GyoW/O1ih378QazBuvujLN6Qn3V+e0qp3Q/wc8lASUzk39Lt//BG+8IkX+PKj53g83eMu3OAfu2f41+5pdiy2DerbyVVWdZpFvv1Qc451R3ONQ6B86/c/qSwZ2UmAO9Te4Usjp79Aj9jA9PndwVMge4ozomw0Ain2I94Mv5vPznHEIyOLtPAmdoD3TW8AAHhNnrClD9tXxcn11HkhkWdGnuSpMSaWGJ5hE0wkvLltbX3RwKZPIzcLcmsR1qPokV3lzuG8Q1E4EnFcHXHObN+TsAfzI4t+CI6PckN8DI0WSJXJ4IJHsGzB3LQ44hxOselgXz7Vz4DkmoiWX0sByNK/wPfJjnKur5+XE3Ona1abVnki83FuRU6fIgt7xDNh/w7b+vqjrDciS5xX5hwrmKQ6RG4amV1dsQlgc0E9WCYcSzHmQI9IlKe4rHUSLVrPr9KOHAuE6UqwSnhTbLqYWp+hq1SXcUimCiidQ0dxfSSthFxcFt+FtUxSaHHR/MpbtE8XTvU0+exlv2FMJRMTq12Os+ebo+KVar4ml7bnc6wWMi+hjAzOQ7tRoVS2Ft7l/EUfyeERXgvAHGXM99kdu4DUAIOzyGukH82awzgsfWTTCqy2eMvBauRKRPAAvMznXbEyqOIyoojCpSYyj1ZDkZVEamFHL8eWPu/KdzAKA9hk7gRuAT1jDxhrHy4tJ9QTrHVs2eDpM+pmKtXwZjoy7jI9qv+csdSGVemWDZr+aNrAOLwrC3IJZqMqxJelSqsc9KYEX+9YIS/X4d3+/IHVQLOQDNWjPuPM+QCtJdReS1+nvdkhUMJZWs5v3JOayrs4Gs6y59ersBtAhw0AbSJZ5KwbUp/wwY4aptKe+0PS6k0OVvcMLOXoWcdporVm7+YcO9LOe7EPwdGK4h6OVk205lfO2dY8jn0Ijv9fLbOxWSvnt/79pKOGXaxvwJEjZ6tF5F3ylcPY7jRYOw27OPoG9L1W5LiZX9gm0TeeLjqwId36GrXMu8PG5AbJSLoMpZapr/9Z0phtHOyrL883Widre6KARD1iBFAnUNd5LCC8q7w82pA2vWBVgD//+qdoja//8JcrNuddFpLL/TRv1BxmTIKVCFy9iri6DXD3AfFmwvx0wvzYrS8+WDvzUF3FuFxTHAi2mVZzGTocyGuDwB0UN/+Zcf3+c+iLW8gnn8J96RnidI3ok1Pf6PnlQsexMe3HeH3znR+yMzmRQoJiejVDn/8Xv/3gN3gn/ADTp9+CC1f8lZkiijhvrX8Uy4ZEnj5n3vyCd4c8csx3gCT7sZJJANQ7yJPHeCd8H/Lk6fFzAJxLsev/OngtG6QfS4YL7W4l5SsoZVqNPXpQIALxZkL47Ntwn3oL4doj3kyQoPUverGQ3tiM17aNDDfz9DbTys12XtFJ5Bv4cOUQn51+TUBO90xBYaWsWUg30yVtHgMhcxpZlnS0dPn2d36h1kWL5Ii1LKgvbKVJH7ZThsF1NL4N95JEjgD2qYyQ8N2zQncEy03odbxAzXVUZ5xmK9+TJ+zGyytV2K3/kNFp3OWbYdtwaSZ4CUVhEAH9bVGRqpq3d7jE1p3mKDBuCdjX1ObtA8G6uvukduTzuc+8hJJA7RQodnNFlfhYh/ViaG+RZ3bYkUrSKA4+uiCR6mV/qIRW7THDuuOwdxb389I/M2G/hEPF/ZtBDoHYJ88sNl7DEkO+qXhjQeXBkvPa81ty6/TLeEuEl6y+JOeIZZHpyy/Ziy6GfEnTzJgE6whHuhiTN5nGXu5JnUc4xniPw2Mu0upkvJRNbCScrRB3Uv8JgNfgra5cyGs1C6/7fQU+cgykh2OvztXY9jlqPB17sNrBazWWc80pSLRpXPXHXswid7GTLXynkyU0sBu8Hstv/rO3LVi7cAIoAdO+6UBeOqOOFBEghFJIjMLRieWLgAu2PJpIfeYCq6cLNpF53d1zAxYjeo0lg0aN2UaNjeDERGZEnbURHbw+3hPnGIqE5q+9qOqJBXXWQCo9wCYco5kMgvf8dfRADVyzKXNIOIl4e0yxa4pl2DN403lp2iXYbAULPkmvnDfhYDYn9n78B80a438Cvb1IEMizRQAAAABJRU5ErkJggg==" id="imagedcd3672a2d" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAI0klEQVR4nO1cu64lORVddvnc7h7QoBYaBEIQI0TAH/DI+BNCBDkhRIQk/AjRfAMJEiQIiYeIoGFuT/edU8feBFV1yt5e22Wfe+9E4+R2uZbX3t7eD5erTru//v3rgrV51G0ifQAwOVf1sfELR40FAE84TCxhZzpYWG/wTs7S2p7PFw1AiLJfRACTMnBa/2orRlkG5p4V1796RRPE5NCecsUqjohYY2XTzdfYSgeOTRJNrwr3Eq4XEwSQEuB1x4Z1df+0YaW8Z4abA7TAK1bRs/DeDRCLfk/H94f21sJ9ussIuCG8S1XfJIbRCIcnhrQ4hhbD0IEvhlRRAQCeUwAAwpx5zgzLEDVrMiYRBwzJOCxDJsIxgxuTGdLiaCXd8CCnDJgAKR14cgkzcWrtIVejCsMSg0Ewa0VdqsLBwtpeLpirkFo6KnkHhg2fpJdUCBM+Ea9YhJDJG1gadmS8xXEY+pVhBjmyW+FdetFUsnDR7J9MqKWkpQybfKGDtLEFb/bPmxdLqRmi7FEXVQROLl3LZd48EiJ0+C3AnG8RnipVNwNGHcIQqgMAJMW76ACKrXSD0PELlnMAQLjPwmqoWpmew0p8f9jYIdbPy/Rd8B2pIg+rt7HOOXY5JZO5JS/cjH2eBQH4ooSZVJd9QCl0lqki/+X3fm+O//Uff1J2rLLyiecVpIx/hp0M7PLnZ9/92NQFAH77px9gvvIeGzQ8pFMToIk2cmsF8rZx1667KqiN38Kq/s2oluexlu/ptmGW8QEgfHp5AatphawNmtXexprb3C0fLEqBpbnqWLf/xVdNrOYN7+OJApliVqKz2vt4V/UxjhGD2bodG+fdqk+v0cOsS+86MKlHBg9BUvnpSKGce+dVHJBqu7AZMIHLy08SNgNqfbk+E7wTOp7pFh6U51gTZq54ZJzcK63nnRFeWsEGQv19vBtKFeHdpXb91sCe2N4a47Yn3i/r1pD/dAurTt4wx92V3EAsb/1//ts38J1v/6u6/+af38T5L+FAmf78U/QL6zs+1JxTvW0pdFjpNm90P/z451KC+92fGbPJMWJ8Gm6PT9AjHhoeLoHeAEDPzkaE3uKJRZ+h9KN5OxckPMy7cTyxxoh3jHrScxjfwt4SEUGyEhiFgHRJdxvWdWDFxhrK59h8VLW1UPLyyRccZGuSYxNI/4otErKluFaUCenioEah0KHQGcKSPgsbzuc9rJgQ63BeYy2DWBxc1vMsjB1+7fFB0o6QXI1DQ2nX1aFD9cmE164/wqHDzKmQqLXc22ZYXfv0PIK57RZXEQv4KghchzHtPsB1e5fNy+fB9eXNKVuENDc2TwMKG+/MuDEGwseWd3sK6MUG0caxnt/W/krMgeDiLgufgfB7rgUo5e0tICm4JUiP3XAkLKsuIx8sSnGsaKwDjQfnRL99XrFclrW4lTww49DmCiLrtt1nJ3ppYhmvHr+AerznetUsLHsL7nzstnv/MVaYAUcmb7WDMCvD94iDY/Xd4GLPhDsFwagah0bPZVneOcBrtUMPL1sontiNwVRfoqwYeWEkHK2SPGZIo49NsKFvcPOWTwjnoCcZW5iKyDzRHFhZcUC9E8Nh6PTJW5+tvH4fqhRgg7kRuAadTpcp1qEDjP0PWQiTo0OHYBzwrwr0kw4Z0misJJuPbI54oxEiPC0Q+Yoj+Gis+EASloGQbIfJAIf9nKuwtxeI4M9lx2YUrmjHhhG3u3FzPEAXp63vsbxclh4S9LFsDuh4FUQHNjbPXUrb42tLsC1Y3o4ioDXfyjg5qFqo1iqz/Ei39Z3jWb4xsDTftOZgPPJobPBzCch1qC9uw7aMOiLLakccPVsH5oHUOG3p49inyE2PzWPDHFCl3NgmUBLDO02BDGvKM7CfpzwACI599HcwSNbyOBJSXYn+YJKH8gzsrfLCZJTyNpEY/Y9T5hhrgJ8pPIO/lAMpbnBS+tYItr1J1C/WbGy/bnbSCYUTDJTOHOsS4GQ9LXSAeHd9r89K5NOV5LLrD7/7BVrt+z/9DZmHbfAwZd/C31I9XAJO7xNO9xH+nJDuPM4fBlxe5QbqD4cnCUmL4qKuDd6tBa9/KDCoiI/Ai3/PuPvHfyD3b+G+/CX4b30VDx/dIZ6Ui3yO+Ym16WxjGTfdITOhVvb3s2B6uED++wnimzeYYsL00Vfg4gneW964dx5WldEK1GhVZSbcuZcHf8nCir3CMvcc6w0BZHJwH7zCFCPcB6+W67TmIjoR7kntvUx5o4m1KJgjVDpk761YWA2VcwHSiwnxa6/hXn+IdJoQXwa4KPyHXixcrPeKRN5QOVdtOtvhvXCX18HPPK6ogYxPIuLJI71+CScCcUsidlHoStGd61AuEW6gDuNco+Rgc3ul/NGPfyUtQEu46WHEiL3h0sb29w/Nw/Dc4HLPca7eaBkDxdHj7UW4+gqqiYXGGjYYNLieR3NjaJyjB3827lBleJXh23euzYgHtRamHv8U2PI6uFh6zrXlZe/6oHmwYQQyo+bYHqNu+SDXgSvuGLbQgWBxbCh9N7giBIxkB6lW0VnKJSFld6s0hLryUF6V+G7WMFKyjixq3RYdiF4AgjuXe+rrUJooj1dqJ6qxlkEZr7N4DY6hMDaqrtYj4JwdBR4IdUb/kdDGl0IV72MXQPezna/Fqx1DHXapDO8dqjdt150x6dff6l7xugLaHPVT8opVdUMo73bzaXgDZvWomq1+UeIGXJmGTu5VT8B7/RfjHdGL5ba1BTmro8CCayBMvJEg6JGq5eKPWwAz7zTCvbqTP3jictG3d1KqgGEEXz8r8G91LMN49u0rxw7mu+oxppM3IDY+s9CG8A4Qgneu/vU6AGGGNCZR5YSNlzXmpfbnGN06VMaRh8840FJiQGHqOWYZ7TckDynLo8k2oddz0mfEOIYgR3OCPanq2PeGXNWDHVkEOOOHSgQf6o9isIeO/gbmWg1yJePaZVWjHSv7T+wy7A4+NF6eAjJsMYMNn20rSt6Mg1a6Xd4X/6FZo/0f+2GnjigmpccAAAAASUVORK5CYII=" id="image2174594e8a" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAM+0lEQVR4nNWce6wcVR3HP+fM7O599bbcy6O3lLYUKm8LRYxSiRGorRKjRjESiAm+NT4iiChIjA8UtBQTjEZREokPgs9gVKoQTETwwSs8WrGlFmhLK7a3vbe7d2d2zjn+MY+d2Tmzd7f07uIvmezMmd/vnN/5nt9rzsy9YsfzCw0ROULQSjLXEvFi4bXIF/IW9GzXwc4rLf2GfeT55cRWK287Es+mwEk6tw5oV8Smtg0MsINXyDtH4NmAAzt4rpeCJhZrRL/DUlA3hqqW1IwLwFGOz7Ljdicyt/7zdax/cg3ykXkM7THUxwXe2Qe55sy7ee9JDyR8u3dM8GIQKjskFMNSMCJK1EyDNDXB0knbkYt3WSc0V+TvWg6AWzc2O4F5UjEiSkgaPNI4mj/sPx2J4csL78vw3fjrt/PMZ6/Iyd/wpRHeSxOcIeFw295zATh33lbeNPRfhmSZmmpQN2njbZ7bNZt7mjE+AG41sog0ORjmoQCoGcN/glG2HxwDoGo04ynekefsA4xuy3przSh21hdQD0qsGNxDzbzAEOXons3dct7eM6qZcO7i3m2vsGpRN6XkfKk7yVI3BPH++jB3Ta5i84FjWDX2PDed9bPCQa567J08tm8xy+bt5ZLxv3HeQADATlVjezCS8I0KzyovRajaWUt3dDO3l0xbn1sIgFs15dxNB8NDteN5cnoRJw6/yJtX/iq599ATF/GdV/0IgD8BNxUEVID1ZzaBG3/sHZy//JcALAN+9MRFPD59LK9ZsI1zBrdZdeiX8dSiUONWdaWpTIp2eQt4cs8E8phs+zO1ow5pwE1TE5nrHd4RPP3fozlucJJTK5EOQttEe05xHJZ1XaKuS1R1maouUzcl6qbE4sokrzxmFytG/pMRXDw4yZrSJQCsXfGZtoNccO5XkvPT52czztKBvaw6ZgdHlg4mY1Z1haquJNfx0WuqmjJVU8atRZYT+3dsQO9Z8DBLzvgtM7uWsuyWb/Dsxz8NwL7xxRzR2AncAVtAtLF984AArgsvVsIaeTH36DtZest6No99i8HTf8/eHYvYWDs20kFndOgX1XW4IOKmTRdaikDDJ0+9N7lOA2BaYkxbcNrwpu/dseVs4kWKSabqnMtP+mvhGHNBv9h6JgBuTfXf32uROzm8TGJOZDluTZdDYFoM4GMPX8I+f5ixcpW7FpzBW054EoBPPPJu/vjzV7Po/hmeWzeAuaY4W53wjZtY+juP3a8Z4E3vehBzVsj7x20ncdXUO5P+jwymC+sa2YdFi5OUuOLRi3Naedpl06fP4L77PsfaI97P3ZM/SO6tO+0aNj51fXLd6jppanW5NO+6FVexccvXOf+86znnlkcYcvycvIzkrzvjt53O67DQNzddAID4yEOXGsiu0D5/mJ++9tbkOj2pdhNupXa86XuX/vV9LB7Y30wKLfS1VJ3VC7rhqbUAuJ7OPz5MNQZ6qsyUP4hXPphpKwKqFxTHYXdG5esIL8gDNpdUVy4zOlupy34+W0W6uHUVAtHPlQKYUaW+ApKm2Jukr1187VJXJeqqhK9chDCsOeeLVsF1Sz51WBRYe+SHwt/TP8+Q6+MplxlVYkaV8LSbOXpNsR5u2oVEynq8r01ztn8N8yoeH55/GatHtwBQf15yyj03M/5UwO5XO/D54kGW37iBiQcVe09zOfoNO/nZ0lUAPDB9IkwN8FrvaqjUw35fJhYM4EcL4nqRUqJFqR+e9ONkx09g+G7UnslOd81SIV8d8f4auN5eIR/cuYTLtl1EXZWswPQDrDgOuw2d3VONp17V9r3WvTsWMX4Yty09E+Brl4Zu7vv1O/bUgxAc6QcuDeUkhx8d63e/kbufOYUrH72YlR/dkAh+Yc/rD2nADZsuTM5XfnQD1z3+Vh749/F89cXVTHkDGR085SaHr3u/WeprB187uIGyW8iftq7gvodPg+GAT33sd3zy21cCsPzp69s+iKYpzbfs0a8mvLd+4nXc+Nhabp86l9J8j7HRWs6tY+qHW8WhRqZXrKEcAi0JtERPlRje7iD2lfjgguZrC70vv3PYCcldzcLyAyffT2OywtD2Eo0DFRpK4gdOcgSqefg9rrkgrPO8wEUqJUkfQeAQBA44Bm/coAc1P50+NhE0lUN7EAxGVHL+k3+dAxWNN2agZMJFyQASgyQpsuy5pNhAxAl3fDlrtykz1ir093KlwfzhGQBenJwHuwYoH5B445q3nfd3bl51Z26A6x5/K7f/ZTUDe1z8+RpnUY2FR0wDUPVLTFcHEAJcV+VkW9/bbX67veaaK1r5m2sBcFXLysSKlSsNBofrzHhl6i8M4/57FIDK6gNsvjIsbnbvmOA9y99gT+evhGefn2BJVA6c+Ztrmbwn3EeuLlGMHDfFcMWn5pWpe9lHmKL40ytqREYhjRKkDx2Eh++71P0SSkkQoCqgBsD3Svxj+xIA/jwzgXzF8dYB1i27gvtnjgNgy3MLw74Gwj6Ma1BKJsBoJTOHChxU4CTXvaY4xIglt93QXKYWcxauxigBgQQV3awohufXWTA0w/7aIN7WURY+qJl372Y2HvgBa+e/j+rrT+aF1Q4DJ+9nbGiGaa/M5OQIph6lZdcgSwrpGLRqHTSv7PZLrz2MU5+dTrzzS6EqS753YwhOUUa2tReZvZW3qN98HwXfKtjHK+AtdEkLv3W8lLyLLhrF3my92YXyzfZ8H7leCnlt/YbSuU39try28ZoCroi+fOjGckwOjDZ9tAW5hfElWWSsQ+vuo42/nb4pyxGtPh932kZQdLSS1m4L20PAO+i3XbvtZtdWnbEcO5NN3thstCuLK+I1nQFe1D5HgHflVvFpu0l2Ip/v41Bce3YdxCH0m25zc6+FugGpiKmD1f1/ANqVQQeMhWmz84GKALO6b4vAtquvLOh0buj4DesBi1vZFqGVcgCIwjqya7DaDNszipOUK1qf+w7FJQqrt+JSJaZ2WbFfFGPiyuikcIWt0pmf9rKzgG2PHf21nQQcEVDo+50GL0FzsZvW1CXgdBnD5pBkFGoSy+lImQjENI8wgGn+IsDIkEcI2hpBJ/GoHzYU1365mDN7im0pJGNwdPM+GoScffLF6Zg8Qw8p61a5u4foZvGkDaC6y1y5W30MOzIJyEFLZujA7zPxRQqMA8aJZCMrEgqEjp5+hXjJca2XVOhWTY6W04wPhIcQYByDluFGqYkBMCEwQoVAGmHaB+jCUqE/KDVTedqtuqhFwkkDBqQxxFkvsRxNM1BHgbkT18kE+z69+ZTRXLJu1aJsvHIy9YcbRgiMDHmFBqdhkAEIZRA6zFTaFWg3cjcRApTOZmE/dsX6ZCxZHVQ431wqjyl0kegpJ54YkRsJkUza8QyVA4rSlI/wFKbi0Bgt4813aAxJkM0YJKNBw1RfFISyOvSDYm9KLCcXdNNLaEySqjUCUhbh1g3lfXWcF/ZhqlXE8DBiYgxVGSIYBB1hIAOTZAEjwUiT9JGmrFv1h+KYI4WKVjVoPQxCmXBSATh+fG6QkQtJZZANg6x66P0H2Dj5ffT+A8iqh2yYxNVCqyHkDZp9hu7Y/LXqUZQw5pDisV3ZKN43MdErozgtx4gaR4AJJykCDVIiymXWlC9FlIfDa21wGgChxcSghPLNKrplSKsevaZYz0K3QjTNK8k6saDQSCnCOkYK9FAZedQYzoJRTMlFD4UfG8iGSTJXeG6anRvL9xkvg2AMqSJQBGEwyegl2z0QgaOybyCCkRJiYBShDUYKdEliBDieTtJ7LAsgTGSFbZ+7+odUznIy1Pp6I6Vo+ASuk3YjQZUlZlCQrXMM0ov54lHzE345pvQ41Liy0TKBVhIiTOnxK2sNRHWPEALjCpQjoqfxyNWMQQQGqZob1EYKjOXrFVF40b8Nr6TOESrrVnlzDkt/VLYpFArLZMeACZq1j9AhOM1nq8iabBO2fCfQT5eClFuJRmrWIvf2qNC+Y0sTAL5qH6dEOvDEbdFvbptW9O2xIVEhB471hV0RMFkQhdaJqyHCXS7jiETenqY7jz+9pjjUJNkqQ23M2ghB5j8e6DD7EB8ROGFp3AQoA15Lf8mwGQ27mM1hphgTl0b0INGh5Qiwu1Bcw0jCgGzErCAn/cWUBqQPlXFMMkgsx6JFkTvFoKgCXiFAtYkXhVYyS789pjjUtLccyFiJaAdKa1v6/1PY4kva+lQxX18ocavAsolsU1IWBAEbSEVkA7pIpq+WE2Limkaj5Y4t9kisQcASe0Q3k+0E8D5QDI544+jlmSBRODlbEC74Rz123iLQLH0U1ExW3Qr17QL4gj5c4/uzM3YzUAGvdWJFhWMB6KaXoAOuaWRjTtqMxEu0lvSgGfO0AdiFBRxu625+N5iVF9m0EtKF4mJ7h4XxyNZ8+IDN8vYOWCs4s5EVvC7M2wpcAW9XrtBtgJ8FvP8B341tMApWBlgAAAAASUVORK5CYII=" id="image26eadf9e54" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAIwklEQVR4nN2cQawkRRnH/9VdM2/3vYMJGyIQXIhiFCFkwx1IUCASTUyMN72YNWq8iAsYox5IuECAPZF4g5tGD5AFDiQmRmM8GSIha9Yowd3F3U2Im7Dkzc6b7ury0N3T3VX/r6prZh68Z112t/pf3/fVr776uma6Z9X5izdZ9FoO3nKlvL5M0oJoyXhRK1jmMXBtRuzmStDe/C/aryvaTZq1fp8wYYBoSVdtg3UKUVkyOSXNgGitoL18B+3WBQma8a3IJAprqZZlXwFLVx7ERgGeUUDlZWBhJa2hWcViKC3X6rmVNhKQ9ZfbtsZ5CuQkNTx3TfblZC5+aI2WdFPI1IZpbPT0TZhsm7uQ9a7V1BGbLADkZHtlCcAAICM2kqCzLY4QdOKPmHCh61m15RsU9nFOakEOS+sJsyECJzZSgC8z3LmUnOUOdL1rp/4gN1AGRQTo20iGzWJIhe3FkJbdAKDn1ZRekCa/BGXj2sMOW+86cJap6KYocbx04GlZTTl8sJc1J0uAcvv0A9yY7+Kt+XE89Ydv4MIPnvA0t734HJ588HU8sP1PfFBt42JxLGp3FdjfvONvbF7RdubduzmoXlx6bieeQLzLNAE99Nl/1A7eOUHBAMD5Hz2O99/+C+668xIA4FfnbmlsB1Z5VEbwRUxtbjlhmaZnZisoAPwVfffCTfjc8SvRAK6bGvx/378Fe9cmUbsAh1drNwOlbW05CcHWe/1zjrdyPJ0f+t3juPVrBt+dvgLLz/8AgLdu+Azu+/qzuPTrHCe/+vvOLoUSztYx2pQ2b+/SgYXRM+Pfrdhtrx04q6Z479QpvAfgT6//NBjAvbdfxJ/xBPAa8J8H/4pjk13frliopYPe6E+DwTZztxXxp6/34LCq7YK6utheKZirix0czYteMJHt02sDIIPiuXoGzcxWtIzovYp/fJBub9cWR1cK5lpxBDdMJ4HDWHxhYv0pbc/qaBnRbdHsHLdBdh/5+sfwueEwo8GUGu5CSMd7KSOkBVulzQY7hvvTc6Odix0UNmidVWsXIgWKnD3rgbpuptEyohdkW41duXMXbsYXj18eFYxSFsyX5C8ORf6qZUxzs5iB0ntlJ1LRTKkDuvNnp3Hrmx/imcuP4CW8LAbw3NmH8eXvPY1L9+/gSF5iXkpbWPLH+sOZPba1WSzBzpWFXlR8BUJBfv87b+CRH/4dJ899Gypwx7j33z/Hb3/zIt7c/QJevXKC2F1tC69zl2pbN285Br0wQzjSkc4d+Ok8w+7CP/UOAihz3JhrbGd7KBIWQZo8y+xV27x3I5L86UWzrSTHLPgzV+7BH69+HlNtcNep0zj7/GOe5u4fn8ZUG5w8/yjm5QT97SvZlTIlvt3T216po3NW95z5BVWwgbKxceP7jlfWUuX+wNaFu61GBp8SZIrd2sa48ZK/TcAGAG2M9GgubULtd9NmlHY92Cl2axsp2t45x5TsQVkr6KxKz+84wBTtwYW9zBzx4aUUEOuTiuQhha2tqUd5MuX9ZWDMRrXUL4UtSj9h2Nq624qmRD3I/WKLOllq3X7fgUrSEl89f3S4258AGwA0TKBekyD7xmLZw/45HH+wYXM4ofubKxBXI9R3OGBrVSoiiAQDwAqpmGKDCg4C7MaGVsK2spG0U4hAlfoPBez6H1qVXOCOtYx8ApTDCLvbVpGA+pdjE42NH9o4uLA1/bYxBZQ08P8Ats7KsEB0Eg0yMl6wkQRbuLwJ2ACGBdkTrJTOY7Ry30GBDUAuyKMcKyIQtJLtgwxbZ71PYqJzIaBRjiXnK9SOjxu2VsbrW8ExH5IK27XxScMeFGQxoBVTdJOwk0Cn2pDPORAnlJQpgnZTsNfbUv6QMbAH2yoYUMrkUwNaw9/Z538iGA23Lz35Ao2h33RW2qBgzISsYiS4NuRrrL+odkRry0nIH88cIRjWYRWglB04CT1SSpu8IJZiS2jtvEOwvYKcWjukAD39CrXDe9S8Dg3XbX/eQmz1thKBdBekLaEqC2WAzKB+PV4pVBqoclX/Ami/jgRrgpLm3fc1OAS6AmnlltctkBUWk1kFPTNQRQU7yVBu5yh2cphpB1g1vyuwSo3KnmEccW1q88oJ8TUoyDQYAO73jVY1IgvkC4vJhwUmV2dQ1/dgj25BHdtBpRVslsFmjX3b2g7fAMQ4NrilgK4gh+zKJ+RBxxCgUs2fFZAtLPJ5CfXRDHY2gyoN8u0psoVGNrGoclUX6P4vUkihHRR0GgPXrtoy49813BuAzthP9VhAWe+SarZHZaGqZrzOoaZTQOdAe83Uv42q4UCETV0GC/X6jX1V45aR5baKFkkz7LOqriPKAtWWhvrUDtT2EdhJjmpav5yQFRY2B/0Z5gA28TfQtnVrhHZsy0obLSNalRUXkKfvw9TvKJutHDbbqrdOrlDlzSNmY+XCZ/w+318LJV6nUpsy8TIyPCH3Baaf+pGVU0A1zXr6FkzPdgT20K4PZRN1pt+W5SRYkMvAK6ts5Zynx1Z1J8EWYlaROpYCe0xNYu+VJDRFCrLrSGcETle1STo720Q1Whs4LffEjSkZthuDCFCa3MiWFc68CWytiv69nAQkfL5x01wJDmotgd0O8moSAUj8hWIb05blRIBtlYJ6+MQvwy+0OI1+GJReDmPakbBruwkxADx1U/y555xB5gSNkaxqoQirv192V8ns2m7X2b8sZbZGUcpOpA+erZOPGcp+2ZVga1WST56SYwZFfk0q2BeFnWIX+wO7y5xQQE3wagNQ+u0gwwYAjdL9tisyMBOqbwKUwwAbALQtiqCg6yc5KtxNVBKogwtb20XR04SdDw0Kk8rI72iCwJ2CsF/AV9lWdrEIC1NWVtDSiUnHfwG65a+hj7eRAr2xrW2vIC/fhUrMlJjDZSalrOh+gc6E2OjdqvJv5bZiYsOdGR+mlQIz1SiYtdZQQLVtR28gwKx82wajYSrQ/6kM+Ir6FhmcthU2nYGddr0MHFtb/wfnL5NtXteTUQAAAABJRU5ErkJggg==" id="image5b8918d3b3" 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="image3232f1a745" 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="imageae18cef05f" transform="matrix(2.550857 0 0 2.550857 118.820571 242.941311)" style="image-rendering:crisp-edges;image-rendering:pixelated" width="20" height="20"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAHaUlEQVR4nNWcv6skRRDHv93T+94pZySCgWCiCIe/4BAzE8HA1P/Af+BAxEj/BAVTUxMjg0vFwMRYBA0UETlUUDgEvX27b6enDGZnpmf6W9Pd+3bf2y04uO2prqr+dHVNb8/sM78+eFqwFQsuldZuTNSm2yC6pL+qq1jmMXBdS+xWRosYcF6CTzxWXUTiNmXAANElTXocjWKWDM4ougyaaLqA2wSRbASoSMQa24YMwjNg4NnH+gOAJTYaZeasIbqi6JKxsczrxK1kHDaHow2YOFN0GeB96LIlqNsgWSL68nbLZgGrpKEekPSG+2DIDM7ZsJBoWVUFNvrBT7KsIuMcQE10SXOYSW4liyhIHVbczkBpNg4Fe5TZo4FeDbZ7JGezQbYdyUBT2TYa6GnB7sStmjN6QRt8D0rSuqcO2z2awOlTcVoPiOPeQaTL7gqnB9stm/OtQj6USoHCgzws7Hee+y66liP3f3kxCbstyBMF9Q6jBs8Ger2wS2VaTlimuaU/n1UAOJSn3L9494Vvk0F8/tPreFjfzrZL9yLQYe8qXTmZg+3W4oZP0czp6fzard+ygnj78Qf47J9XxnYplPxsnbvD5Mqqu0vPZLZb+vhuxW570xl96dnfs4J48pk/sPz+tdZuUaFWbr3q96YyWU6XFfHnLrZwtIqt7Q9KZNUsorbk8gl1w9jCu8wVMmjpz2dBW9PArRunXmSi7Trn5MIPcPTN2ExRz2wvkbW4ZBlxYeCt4y7I4QvpLkBCufBnRbC1jNBs7CJhOdFgu8tmepgwfFZnrjCd102FHNjMrp49VwOlTVjoz618XA/2PXOdjxLYaSja+WSeTMsJG5tb14OSYSk+ahsC+vTHN3HvztfJIL74+S4u/8yva+k6k87sHLnoJ0yHTZZVXpDfPHwe95CG8+Xfd7HyAxwNNvO3r2XNZBi3HoO79GM42qHhtONf9RN446v3qe4oA5e8v2YX0AfPMntXCcuJ5s9dbpeV5pgFX6SrBFeSKenlXi7r2iXHYV6+/yHVYB11Y3n9Q8c761LNw8B2m2BZaUFSUAVBqjaOGDYAOO/1h1olA+rOpX2W7mnAdr5mD8U6hcGq9niHB1mie/OwNbt95qjPtrSAWJtWJE8UthPf9orUTPSfkTFJ6lK/FLaqesOwnUyXFU2JtpNMLlInve60PXZginSJr8Af7T5tL4ANAA5+pl6TIENjqexhH8f9jxs2h5N82yJQUGdjru00YDtTG6KgdRxElFSch6LJkcHeijPKslLe4ugdG+wGVdM9LtitOFNzhWlfYeSLMk3TPV7Y7bKamwkCKzXQVP+xjSODHbQ7eriX6DQCpXUssXGksJ2t5xVUJ8kgE/0VG0Wwlcv7gA1MCnKksFM65+jqbccCG5gpyFmODVFQdDXbxwzbWZ9wrAmp4/sN/gZhbxWd8ftwzLsUAS8q1LvEUA57VJDVgHZMUW3VzfZX/O2S2dk21H2O1/WKMkXR3RfsfWd2Duy4IGsBlQy+NKAr+Pvh4/cUo/Ny54NPaAyhOFvLrELZ4BXl64adIV05mbMRLau5YHjD4MRMDwKuE3ahsBvRNLaoIBfPKLZ3yYCL2NbOqM8OtSMH9q4yKidKbO2yUoEMF+iSEMA0AusB4wVG2oE1zkAqQKyhv/hpbbOIeZBFu+RM0cYd+uo3gUwhNXOmaZ1Ua0G1bmBqgTgDf25R3zJoFvHPoTrgJfUnR7dUonJCfI0KMg0GgHbgahpBtRIsHtVw/21gNh6yqFDfXgBwqAFIZUazMQJ+A1A66ctJbkHW60EMEACsF9haYNce5mIDs6mB2sGeVbAbgXUGIhIvCwJ7DJDFwHV3Fev5CWFYSpzd6F/jR51sfMnWAlM38Wl1I20tqgUNgmOnGdhlhfrqoh3VhJndL6vkLdbHbf1vOioDeWwBWVSAsxC3fYrqBVaQDZs3KHVK0c0VW0uyjDhTN1yBPH2fAjTS/hNn4QGYswpiAHFmmz0G0Y9k52BH/joo6TpVKsany8h4hxwq+DD1lZkL+ogb3o8SY9q9Tx38bDUD9mAvhrKPOhNKX05mC3I984Yomzn9jZUBYhOcnZTALqlJ7L2SAjFKQQ4dOUvgDBWbpHO0Pwg3iqn6lQ87CVAbXKbYzWTcBLYzm/BezjZofKQUgOUDorA7RQX2dAPKd8m7Z09fTmZgm7de/Wj+hZaJ0C+D2lJjujvAzooB4PWjxN9Ed5w5s8ZIVnRQZpbaIexeNbOnNrTMdtjUenpqXzw7J9cM5VB2NdjO1OSbp+aYQckAy9qSsEvs4jCw28xJBdQV2j1ACeWYYQOAQ80OkWc6WqX6FkA5BdgA4GSzyXIMQ3JUuZuYIlDHC9vJ5WZ7fd7x2JgyoK3u+I2q04XtsM2caLPDnBQEdDDYWhwMtuJPjS1aVlrNSQSccibAUQEexZbM5laceOXZjPZ2N9ldiRaY8ieq6KCJrrTKRJf5axS7eSAAoD18GsRpA4B4Gpg0LDAPw2aT6vKxmUY5HWDZp+gKyz71b5uRC5N4/wcjZVBSqP1NQAAAAABJRU5ErkJggg==" id="image8b367143a2" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAL0ElEQVR4nM1ce4xdRRn/fXPO3d228kgK4bFNabeRWhpeFWyRh/JqKwYTSKr/gJoYhEg0IkJFg7SJEEvrFjGK+I+YUuMjMdGItBBAlPiHVB6ivCFtpd0WApRu93HvmZnPP2bOOTPnzLn37tK9p18y2Xvm8c03v/leM+fepff2DDIcikAIkYAo1UVU7hvqZ+rLfSOq6HvS68H6XlM8rlWpMrRoQJWXzWEwRXB8oK/dlhKge+ZXyBAGPwQ8EAZ/KsDHSWkiYHDeiFe38pw78OiOdQAALghC8BTPo6q+K8+8Hduf/4HX9v6eQQA5gNphe8zgW+3WcNgpnZpe3X2itzoBYNH8fV5nF4DDAU6o7e23Tso+FzXsuHl7K+eYCZrcuxAChHhUN3o6cRWNuqpiQYzC1jLjdEg3AQDigJ4FtxzkfjzwygosfPBOLPjZRtyw4xowKCtbXl2O86/eCAJj8fphr61Yhn60CQTGBVfdjQdeWeG1fff5q3DK/Xdj4dY7seXV5fhAN7IyyjFGOcYBbUqv6YDWGGUNeviNJSW7uP6xL2P3V2/Nnl0TWLDlLuy69rZgW5FcM1r0w2G8/p2bs+fPzPsGtr31YwDA0IZhbP3Cvd7YyBn7iQW7u1rU4aKXdp+ECIx4nPsBAAI6axwYqTY18XbftCacvdcHMQUGAGaPEMZ1PwTp4jBEKNfNNE1yBAAQB9UAxnQ/RvWsrJx8QR4dTl2/2Rt42rk7pzVh3+p3vOehjcPZ59mr92OM+zCqBwJl1rTm+zA0qvswqvtA9718USEJ1LjuY095nXsRrX7z2scdGXyen//oM5VzzAQ9/MYSRKQRN4+QaDXpyBFRNeC9oEluIIJGPK6n50MON43r/uyzqBmc1P/Fo2oAUcERfuWfX8JfnzwDjYOEY87fD16dm8B1T1+LXV9bhEefvgNn3TAM/nl1tFp6yzBe3HgTLl+2DoP37QIvz/te/vg3sfvv85HMYVz6qeewWO8P8gg56ZmmMbtR9K1n15S2afjs33nPrn8o+phuQ3knPutfuNJ7Fk777ac/VDnHTNA9L14KAIgnlG9WdewUAIxbOeo2KQCYZJN4xhOqcUQI1AxkwnXJNa6MWcVFoerSnAknMIg26UEvaFz3mWg1qQw4+S5FtQg0oRpBUOrQnlRh4lYNB7sQNVVRg+vTngllcq64KX2hiBjLHvoePvj3XDQOEdRZo/jLsqW4YtF/AQDnbV+Lxj1z0ffIM9h/43Lg3hLvjE5buxmDm3egeckZaN30HrDK1D+9cz6WPLsO8j9HQ/UDJ5y5H2UNro9ShYmbKgYVBPrXZ+/ynr20f5UTuu/9bfvjwwbbd9uvgW0OnwXlFOCKJ79eGl8XUJnmJDp8yd1rcs2qboc8KRsQpCFaMkZaEhUhURF2/u/EyoHP7Zo3IwI1nfmbKvZKr6mlI0yqBoRUAmlJZIRERrhs6y3Znd0lF9/l3eBd/fubvPu+djeB7tPidf6t4YWfuztvWz+MRAs0VeSVRAvUodnppoh0t9IitcD87c2s4xNP3OYNnPeEnNaEQ1v8S/Kn/nRL9nnhgyNoySgrUpmSanSvKdXiWEqB4iuiieOrrzHGj5+esBNDc4E3wm2TQ3Mh1ViwrQ6nLK22xlqV1XbvaoWzjx1GY4zx9rnAhstXYe3S7Xh/zyDeeSTBqm03Yvu+n+Ki1RuAbdWTfPLqTfjHH76NVcffgJevPQXv/vJkzJ23Fz956WKc8tgmzH1WQA4Qdq7QmFWQI92w8ivHmaeWNIkwLdh6p7c1RMD1Z/4Na5duz+uc6LH+hSvx/dP/HGwrkhuuH31zMVYOvRxse/zNU3H9jmvs/GV+r625o/OKDiMt/ePtAICYpQDIvb4Els3aWTlwycD0XrBdNNCqbLtwQCKkwSGgekHSao5gRWAp8qIE7h/5dNbx4B4/dG8ZOW9aE948ssJ73ue84bx13znQivKiTVFKQAVAm2mS0sxL83+xwWxP1Z1VqD60ox92PFAKDO36hvhWalqwb+e5YujQyPAcwQ5TECivKzRaHtxV3xDfdHzhbUfbviHZ/AExyW4mL1dxBsr0xld26CXYldpuRhpwKjpxG+Spq0m6rEOPwe4or/kQUyiRoDIPnhLy5aog0EBvwZ4KXxTNqs3kYaCKfat3qTjkiAS78BwHr4w7MPOAqho4FR69BHsKLiQWsnOndgul4hhCIBRU8OgS7DfX3oxe0oLNmwAEHHJpDbbCBcDdUBYMRAAXcjVSth8TQGzGl7bbqeuoEb0jITs5ZNefFQOa28a2M+fjSAOkCflXa8jkHJRh5VH7qNh7SjGJRQGcKe0UGUacal86lgt/AV8zUvC7jYo9pgwckr4cnWTjIggMCG3+Ett2YcysI9CB9q4c9QxTZlaVmlPwkunCiax/SU1IAUIaU0q1QUcAxwBHOT+yQKa82y68wv/0ilKFMZoTECjkgAkGGNKO79AWmNS/sAEQyvmc1nPOm6gAHCoAqwGh3KwqHHJV1CrmRZwusnB7Sjrc12FVMmFvzvpcDkTmkGUhdBSdKpX9TGYiBOiYoBuAjpGZmlCASABSeRRjQZm2eCbmzon6/IxLebSSYYE8E3Cca6YRbE2MGIjL0YoUQ0gYcyOAI7a+ikDMnla54PsOuR6kUlcTk6owodTPEMA67DugrYZoBrfyxQjFJgl0nDTYgJwxdjSniEFufvVck4rUIXtm5am31QYNCPbNIw3TpIEoYYgEEJKziKZjguoj6NhqlzU1OM6/lBI489dtWaTsfU4xlANW8Mi8zUy1QChzBNANQPYTODLgxBOMgXcTNA5MAlIDsUBy7AAmj2ug9REBHQFQBpwoMSBzZH2V/SpQSIZMjhooPW+WHLIJs2RuT4lz7WixyVtEvr3EQDyp0ffOGLBnH7iVgPoa6Bs8EXLO0WjNEZkaCMWIWgzSDNUg68so80/eea1mDUr9sBDS+geVJ3RpnVDms0jY/6tg8htl6tBsQR8awyNjv4I+NAY0W6avMuCKlG+S8nB4W8ddlgFZv15TKkNMMuD4HJ8gJIMkg6QGImNmIjEOyCzKaANI4DJaA1AfSHMWrVJ/ZAC3YEmjhaluiDTkF2Soi1Jr8szKPTpETfOXtFl8mpuIhBELhpac2aY+ahaik08ApALiCPoo82OOqMX2lM55zgPzLCSyyEeaS065TsqTwESXbVxQdgdrjgj2PkYzSAKR1uZXdDY6yWMHQHPst0GJoBsCHBGiSQ3RMnkNdCEFaHHHLwnUledkmkOq8LaHACh/J7OwDoASBrHJ4JhMWE9mx+YkbhdDyviSqKnzFCAi7ywVcsC5DJZPXXlOYhLWWCTpQv0OXoYckbdwYwYMFgTdF5ljRINMJGMgYga1GEJqAzQRODbt6fEhMyVyNCSTIWDqPSSy6UtMSvtylXqSMad0AWzBAawP0sY3Kcp2XCQaQmqQZPMbaJsdkvuLVm14gQgkyhqSbcZhWOxUKTerpBArC3ZOgbq8kUFKQyQdlpAeNFXokMv+l3BqNikAZlMRAgcIg2F3GWRNTKSHUG3CvM5PkhxFQCzAsbCHMzZOuThHB4dbW4bcMpi0B8cuDIAHDiw4sOAgkSCp8jGxtleBzm2X1vmFmD2ddwKnY/sMkZBG0BiJDAtRaUpk0v4UU+t/OCq8m9EalDjguJqjkWtgG6r4PyAzTpSCk+141tI9UPk5iwAhfCC64Bv0Z8J5ruMLgXDAQdP5Opqo2CrXBJgzLSAiIBJAFPmao7UxM7bRSlDZjFxNcs34CCBqmdQ/Zm+HA1slKA/jltgFRxEQM0jnzhfMgNKAcvi5mhWUKNAmagIrseBAymohyPiWbI8LToBhtaLVKgFYIuU47BCFtLamcJXlfivnfNGPscEdDJtbEJBKoAM82m1KN+MreFRu1BTWBiLEemIiODGFBC9pzjQEDAkzhb7VfPN6b7e73ZRwwMg942W0pqtBXYNZJVyFgHUCajoWvkXvh41qCgJnZg7UVZhhF9qY0REAXtfgzDR1rbXAh9fcLoH/PygbbZz+XU0XAAAAAElFTkSuQmCC" id="image6bd3f8e689" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAIhUlEQVR4nOWcTahkxRXH/1W3Xvd8OuNHhhmToOIgKKJZBHEZUCYQ0U2Ii8kiWQSycKWgKCgI4uDGwaULtxIwO0VRGFAXWQRJyCKCiKIZJw7JC5mMb96b19237nFxb99bH+fUvdWvn9MvHhh4XX3qnFO/OnWq6nb3qAtfHSc4UigFTjTTVkDQZWyIuoxlOYZYVwt2C8XonviM1ZVE/SOAAwCFoMwFzUGrbTC6wqBZXcEyHwOvy4HjoAE8OPXp+eMkDpAZiwxDsrF3gIYwzTYVgYKTSDQ3HiVXE2DcHrkkamywJgL9Rjduamz0gbZObI5uYyMHNACYTTKMcjBo4kHUDuN2VpcC8HPdHPAk6LIZzseQk+FmqxrHhlXFG0DcXggBczYkaCyIpUDbmQ2zSaO4U6CnOSgiwNhGFmwphqGwxRiGgwLqzDPb1Yh9Uxp8CyqwyenvddhmM4DTpmI4eMax58AtnGxtGQ47B5RnY4C/HNhtzdEDoRzUE5y+46P2tRLSsjbVFbl3P78T6/Y60a4XZBSDDPuXJ/8m+k/Jm5/fzYNy4jLbtBYpyDtThVvN+kLBHCuu4MvZD/jUF6DwGcFPYq6E5YTLNLNlx71K7oy2MDNlk9Ywqfy+cqZwMSwHShtPAydVRswkPOd4KRZH8qeNO/D824/gPxeP4KYTl0EPCac7AD995xmsXzyC649t4OGNv+OAntZ2GSipbB2qmyPb813as+tPitmy8W7FH+zqjn/48/04//snAQDnAQBnxAD+8oszrd7mi2dx+uEPY7tioZbOLrx+rmyFy4rxZ646cLiqHYK65S1qoOTJ7a98iq2H3DOVAIWbGGFXlAAOkS07liemaTeTKr4+uAqhjN/+iG3vk/fWX8VJezpxGOufmL72HJmQYTaA4Jxz1QpF0rmFuMfwc/THxQMKJkI63ksZIU3YIrLlrRjen9m2JlDooHCdfvbgS/jg3NPZwZw69FvMJyIHipw9OwN11Y56y4iZMssqNXNfPTACzuUHc+Gxe3Gk+oS3uxAU6X49TMIs5kCZSdkpKSYgP8gCP77vn7Cvn0H1zRqKIzPgtBzAyTdewPTSPhTXTfHD4+vYLqUlLPnj2tOZPVTmWZwqI2Za8TMgOb710H/x/q/Ptq+T14dHuzPQ4399FB9fPhHBTvkTQS3hnNONW47BTK0PRzrSzTtKu1ufXK1GmDETwUIRBs9l9qKy7WxEkj8zbZaV5DgM/vzG9Zh8fRvGN3+Byde3ATfLAfz7wgkc+9FFAMCXV26Au4RzMqV/uefLpDS9dtU9bz7LeuE6ygCH9Q+dL6TLau4ObDMLl9XA4HOCzLFb2xjWX/K3DNgAYKyVPijJG9D8+bQdpLsz2Dl2axvDdKOCbEsGTqvUWRU+1RAA5uiuLmwvc9hBSQFxbVKR3KOwDdnOs6eqoj88Y/26rF8Wtqh6jWEbCpdVInsoeDOVaVFICdjDdBlfjj+2e9ieARsADEqVsCa44wYl6srBrDpsg4or5UJHTkGcjVTb3oBtVKkYhZ5gAJCQijk2WIUVgm2U5SOnnrRT6IEqOuZVVxG2USWvEPYljnwGlL0Iu1tWPQG5b/cNtK+/b2N1YRv2aWMOKKnj/wFso8u0guikN8ie/oKNLNjC28uC7RXkyOZC6TxEV25bJdhxQe7pmJPOex220c5NTHSeCGj3gr/2sI2yUdsCjvkuex22V5DFgBZM0WXCzgKda0M+50AcUFamCLrLgr2zJRV3GQLbW1bJgIYOXskvdwP2xy8/IRhNy11PnfUb+HMOJRWShFXdhXT9BykA1NS++b9AN+Ur6W8ZS82ReTlJZSufOUIwXEMNhEBatY8IVEVQFTo4KtAf6I/ER3ZCbBkyH3cKdlSQc2dUzTNGO2kSZo1jN6d2RB8174RG6NYdtxBbt6zYILvGcEmQ6paPLgna1hlDWqEyQGUUxJ/jYLm74iKiS+otI94hMFSQZo40AKWgiFBMgbUNi7WNGYqJhR0XmB1ew+xwgXJcA1JUg6ttq3hsPdk6RDdXonLC+PILchNQnPp+S1UA0HVdKSYVxpcmMP+6DNq4guLwIajjR1GNxrBrGoDqahAC4NcAylzacpKwGxVkPiDnIxlV/2iLCFAVoKcEfWUKuvQ/vHfpNfy8/B2Kg/uhj46gLUBUg1HWhRJH5KbzwofHDNE2vqGHG4DRM/ka73XU3VtE9WtVNculUFD79uHUwd9A7TsMMhqqIugZgQo/cxprTkCCy2Sh3rlwj2rCMuItq2SRdC+otmlvzjT2wAjqxqNQhw6AxiPY/c23pkoCLNV6wUM1Fzbnz9Pl6pSgO1R0SYmJqVuMKruoPSXm0/fWmCVvadj9BjQ6ANj9QKFQFfXI9dTZEYIzj7ecxdozh9Jfp3JF2dTybT59CAtyq2TdjOJmrjsCkFEo14rm0EP17mTJrzMJ2HHkMZRl1BlX2nKSKsh6VmUF6XtowFH9NzSAirqt282WXthuw4CaxH2vJEMUU5BDR0bZijl0hWuBiw6ABZRS9QAKBWrOPqgAcL+75GALB8VegNLgBoqeBUWQgW3UzH3axR3Q+BmKtt7ETLKw5x2jwxifrfxmsXj2RDcDLrNP/eS59BdaAmEvg9I1gdMdALuzmxEDwGd3jr/wnONlTtIYk1VzKMLs75bd3Mzu7HaN7ttSZhvMStmJdPGcO/mOoeyWXQm2USVz85Qcc1Dkr0kl23ph59jF7sDuMicVUBO8WgIUV1YdtkHJfaqX6KiF6psBZa/ANjSbDXIMxeSosJuoLFCrC9vQdBY1soOTzjHMf9ZDoi4XmABnt8BnQDM0nUaNJAWRM8uC7qqC5+Ly4TjG2mtRRuBDZrs9SewWaCmOHlD+9wfbnS++BD2ofsVFwhtnnC4baKf73QJl4aTk+wTuW5qqiGzXmNVKAAAAAElFTkSuQmCC" id="image56dffcdc8a" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAKrUlEQVR4nOVcW4wcRxU9VV0zs7uz7HptrzG2EyfYjpYYBflBPpAiLDly+CEKJHxAgCBLkcgHisAgFEsoiMRgCPEPn5HgI7IACQsihISQIA9QLAUlWAQLHBy/4pe8Xj92d17dXVV89Ku6+9ZM9+7Mei2uNNrZqlv3njp1763qxy47/8FaDUMcxpAVnmsJdUHoEuOtuhbLNAZalxN2HWbR/chJst0m7GyGHABwLMr/b8Sx984F5FgnSczHTgg1niaEskGRBCwtqSahoq3zU+Iwgsn46rBckMFBvi2wEY1P+imiU7qGrRSqFIZe0SsNG4ZuaKMM0aKhBaFIT9jR+XZOEGazwYnxQEnSrTYIXcqGLh7hoqlqNAimUr/XmQsJhrYWmJZjuOKPYY2YxaeHrmDVhou58dcvrMff2hOY9scwKWYx6cyhznxwaDQzC0KRHmDIt2/beJ7UXaycPLc21yYaupprdKBToTzpzGHScQEA73RW4pljn4P6zyj41DxOPPYs6Wxi/QXsf2Uv2sdXAJsb+OG23+GB4UtBp/QxrUZCX4ocn8UA5Besn9IkyotohJFjTSWm4EBjNa/CYQwz/ijkyVFseN3F+cpoV4fzZ8Zx5xseLqKOxieqWMWH4TCOGdlEW1VIXySGCBsNsS8SlReTB0GDTBfkM/4qNPQs6swDAIxuvYYz9RWYuHumq8O1U1dwVkxi/I7rcJjGMdeHA40rchxNHSwKNyMnVXjzRNkWsB/S1nke4prDM4QkgBSOzm7Gm5fuAgC8sPU3OPbZA3E/6wJYP5QUueNn1+GJ40+AM40H1r6PHfUzOf1U2qQwDDBkQol5MBZLxIx1AfP6+U0Y+v04uA/s3D6/IOdbN16E3rsaEsAfH65iaupS4i8iJVtjLPVoEEJHjkzvVlQ4dzoCY/MaTGo0lcTEAgFU5xVUhaHTEWiqWhIpGVKoIm07MvRLGpld24GC6GTPOQaG6Jzw9H2vYnL7LADgZzOfwi9/cT/qJ6poTHWgv2Y52QG4+/ABDP9rGI0tLr668yi+O/IyAGDaH8M1WS+UOjYC+y0NVcstimjK/FaeXaVvfOzV+Pvz+x7HuRe/afQ+Y3V45vH98fe/PPpTPHfklfj3Z//5MAD7DkUe4AYoHWJjEi2DHOockSXqeIqY4vLmkW8D+I4BJn8yp/wBdgL7KU2V50HYQQ4WkAkGuDV1xpSmquaiVbRkOpwSUmw3LvojkV/qusqWUoNcMOq8J9pShI4jQAkpg1w5l4hYipSlip5osUx/oijISLY9eQj/eOlbpZ3v2n0Q+HMejM3fUqYUQNdA0fGTRkaFONN45K9PYaLaAgCMz1/AndtfwMgHAs07fOApu8M7X/oJRk5X0FovUbn3Mr7+4y8DAK65I2j7lUIbQK/2fom5WFGqC1fRtcUE89+3NmLTr2bBPIlHfn0UT+8+FPd1vXx4MjkDvXZ6M370+S8BAE5+cRxb7j+LXim8lNHTlpWcP+HKNDnkkY4BuhLcb8vuMkVl2h+DHElWp5OrdYnY0pqK7H6JK4m0csO0sjnmTEOta+PUY6NgEhAzm7sc++xyZHoHTj06HNhc14QniduzFgyDJCWS/MYECF/SN6RNQJMr57B6w2UAwLkbKzD1vUMYP6VwcxOH/oH98uGe517Eivc0Zjdy1K5ew32ffB8AcLU1CjNiuy3MUolHlBfuSQfmx1ccvuKptqojMeR4WFFtod2uYM07Ht46vA9r3va6Olz1rsLfX96HD7/toe1WMFFtYaLaQk34cH0HnuTwJIfrO6mPL4OP64v4E2EZlHR8kfsIaYkcIFnRhlvBVTYatgE3Nlewa/dBXN1C33+OZHajg10PHsTMlhqANs41guv5uU4Nvkr7tcXfUkWPK518QZY+QU6sFECe84cw3xyKu2d3tnFzGwMXLez4w348tOHf2DP2Lu6tNHDWr+C3N3fgtctb0LzawumPczDRQhXAhevjeVfWOhP8lAV0+yFUeWF3HT4QeyQf6RCAbKtsBU/ZsBihbJTT7U52Md3wnKNlMiqlynJf4kE5k6Qu6bfPZPf2txiyhc6mVZfo0ZnObpFWjMDlSzYACPisizWLO2pSVl07mOVOtoCiktEykFKwrka3ttuDbMF8Rij0AANAW0KxjA1SYRmRLZikkeseYcfQg1SrY1p1OZItmE8rZMdqivkSpNyOZCdp1QOQ2d1ror3Gp20sX7IFeVu2DFG2gT0c3w5kC+53V7A66Qmyx3iLjVJk22z0iexUQc7ZXFA4F9G1t91SsjPd+YLcY2CZcL7dyRbcuOy1Ou8CaHDgbz3ZgslcW3nH5hBDZxBkn3i+/GOhInLP9w/l5pwqyDZAvYBn65xmgTIVBD19FfA3CDGDJJKg5pTeYon56kwfKzd5qz9bSvVZaHKIRhJQkdVnSG0PhSZf0t+ghFPkcL/LbFAgnDkDOKB5oMs0AA0whfQ+mo22BabaoCS1a1MFmQJDTshIIa01VDTzMHKYApjUSJ2+u6WZxZ+23nTpv3A/jy9XkMtGD2MAUxraqF1x1HSLnAL+co+aB8gVk3nzSVqRIBndZUQBUwBvaXAZksQZlACUYHGqAZYdjbBL+Rv0TgUAEQ+mL0EVIuvKBY1QThAGTAGirVCdU6jMuuCuhKo68MaqcMcc+ENBPYpTTUcAWKn6sxTJFZUX01e6IIeAuoJhLLhXwhiY0hBtjdpMG87l69DzDTijdfC1E5C1Ycgqh+YsiC5fx9ET32spQ8qAGaLOe7mCbAUUTYxrcM6gEKQS9zR404W6cRN/mv059vh7wcfq4O5QkMcM4DJJu8hx9gZv6lBpBTQ44b5ObQAMgOCe/TKeEs3Dfs3ApQ7qDGNgtSr2DH8FrFaHdoKoiqIlipyI4ABDOscXdd3UBwkKcua5lZlWXa9Yo4nxcLJOMlk1UoEzuRJ87EPQtQrUSPAOj+Np6DBimAxthPY0T5vP+TPEujH0UdLnvcCTYH5yGEk55zQUrcKxxgsWsl6BqongTxYZgwpfdOIdnWxT2W1dpn8nfcWkDP5lgvTGFD59yBbkGKg0I4pluo0+jqDwDjHjEBimk2ecAgmy7afhPCmD3s6p8iK4p0qBzIp2WPB6NQu0WBgszNdxAQ6MJNt43JTzF1tN7C/VhSdx3hNMKuLQlb1OgBUdUwxMM2gZFmYdkqIyuxMQbP/5vxsiZSnqTAqGJCKHeebdLgKQ5fomtfV6sNaotA0ihbOHUEu0Dj6tkhKgqYKcEispBVY0igbr5UfB+sO7pGCfhXkqtzDpyAGKk2KmQ5ZfTtgpQ3Zk2xJVgxAms1fKgIDn2x3biidneeCmmG9uLHNSYtdRkJhz/sxH9xV+9UlTdcX+mlRx3UXatd73oYp9CbvdIycDOj649YGQFNGLtGs9vC7Arkm0gE881bOuBPXmaYkJmQVW9tAtMyGKkAXaNXuF9jxSKd1mKQZEOrAyZAF9IZx+SLDIEgBAaNfL6JWpC/mJaauuDRhBjuXMRGIrQ7pN306Om2rQNhBlnFl0abvFSSM3BBsGy39YKhPtCTmGsfjhQgngRVY73hbLEG3RX2yEm7rpdwdThT7/z2seZF8gHBQnqhSpBsi0uz7UqEWSSpLTTUjiAq9EmyW9FrC6aVdLQ9z/AF+BMoBF7bKZAAAAAElFTkSuQmCC" id="image9088131303" 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/mask_image_over_under.png b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png index f416faa96d5f..5ff776ad3de5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png and b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png index cb0aa4815c4e..0d6060411751 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png and b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/nonuniform_logscale.png b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_logscale.png new file mode 100644 index 000000000000..da2c0467864e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_logscale.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png b/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png index f0edf0225890..31e62e4ed4cb 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png and b/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/upsampling.png b/lib/matplotlib/tests/baseline_images/test_image/upsampling.png new file mode 100644 index 000000000000..281dae56a30e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/upsampling.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png new file mode 100644 index 000000000000..df8b768725d7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.pdf new file mode 100644 index 000000000000..31ec241a04fc Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.png new file mode 100644 index 000000000000..a4ce71fb244b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.svg new file mode 100644 index 000000000000..01650aa1cfc5 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_83.svg @@ -0,0 +1,199 @@ + + + + + + + + 2024-05-08T19:52:27.776189 + image/svg+xml + + + Matplotlib v3.10.0.dev150+gec4808956b.d20240508, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.pdf new file mode 100644 index 000000000000..e09af853ea1f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.png new file mode 100644 index 000000000000..8a03c6e92bc6 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.svg new file mode 100644 index 000000000000..b0a6fe95cfa3 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_83.svg @@ -0,0 +1,159 @@ + + + + + + + + 2024-05-08T19:52:35.349617 + image/svg+xml + + + Matplotlib v3.10.0.dev150+gec4808956b.d20240508, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.pdf new file mode 100644 index 000000000000..db06e90e5490 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.png new file mode 100644 index 000000000000..d5c323fa9bd2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.svg new file mode 100644 index 000000000000..1c3ac31ac5eb --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_83.svg @@ -0,0 +1,148 @@ + + + + + + + + 2024-05-08T19:52:37.707152 + image/svg+xml + + + Matplotlib v3.10.0.dev150+gec4808956b.d20240508, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.pdf new file mode 100644 index 000000000000..6679c1e8af13 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.png new file mode 100644 index 000000000000..d6f17be104fa Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.svg new file mode 100644 index 000000000000..3268d5d3d26d --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_83.svg @@ -0,0 +1,159 @@ + + + + + + + + 2024-05-08T19:52:30.625389 + image/svg+xml + + + Matplotlib v3.10.0.dev150+gec4808956b.d20240508, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.pdf new file mode 100644 index 000000000000..22e75bb9b0a3 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.png new file mode 100644 index 000000000000..c23070cdf8b9 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.svg new file mode 100644 index 000000000000..97c40174b3ef --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_83.svg @@ -0,0 +1,136 @@ + + + + + + + + 2024-05-08T19:52:33.020611 + image/svg+xml + + + Matplotlib v3.10.0.dev150+gec4808956b.d20240508, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png new file mode 100644 index 000000000000..f22b446fc84b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png new file mode 100644 index 000000000000..415dfda291dc Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg b/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg index c5f152be9748..9b7644a861c5 100644 --- a/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg +++ b/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:42:10.383159 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #ff7f50; opacity: 0.5"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #008000; opacity: 0.5; stroke: #000000; stroke-width: 4; stroke-linejoin: miter"/> +" 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"/> - + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - + - + - + - + - + @@ -259,17 +275,17 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - + @@ -277,82 +293,83 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - + - + - + - + - + - + - + - + - - - - + + + + - + @@ -361,89 +378,89 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + @@ -451,39 +468,39 @@ L -4 0 - + - + - + - + - + - + - + - - + + @@ -491,8 +508,8 @@ L -4 0 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png b/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png new file mode 100644 index 000000000000..6b587a78ae10 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg b/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg index fbe862667424..9970c51bf07a 100644 --- a/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg +++ b/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:43:38.220504 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -121,32 +134,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -154,42 +168,43 @@ z - + - + - - - - + + + + @@ -197,50 +212,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -248,36 +264,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -285,43 +303,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -329,47 +348,49 @@ z - + - + - - - - + + + + @@ -377,28 +398,29 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -408,126 +430,128 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + @@ -535,8 +559,8 @@ z - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg b/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg index 3893e172bf78..4d0bb1aefc81 100644 --- a/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:44:09.761597 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -192,27 +205,28 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -220,37 +234,38 @@ z - + - - - - + + + + @@ -258,45 +273,46 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - - - - + + + + @@ -304,31 +320,33 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -336,38 +354,39 @@ z - + - - + + - - +" transform="scale(0.015625)"/> + @@ -375,42 +394,44 @@ z - + - - - - + + + + @@ -418,23 +439,24 @@ Q 48.484375 72.75 52.59375 71.296875 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -444,376 +466,391 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - - - + + + + + + + + + - - + - - - - - - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg b/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg index 0c231c4d5c25..6f2498ca0912 100644 --- a/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg +++ b/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:44:47.680020 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 274.909091 200.290909 L 274.909091 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -195,48 +206,50 @@ L 0 4 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -244,32 +257,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -277,42 +291,43 @@ z - + - + - - - - + + + + @@ -320,50 +335,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -371,36 +387,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -408,43 +426,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -452,47 +471,49 @@ z - + - + - - - - + + + + @@ -500,28 +521,29 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -529,55 +551,58 @@ z - + - + - - - - + + + + @@ -585,82 +610,86 @@ Q 18.3125 60.0625 18.3125 54.390625 - + - + - - - - + + + + - - + + - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + - - + + @@ -672,10 +701,10 @@ L 518.4 200.290909 L 518.4 43.2 L 315.490909 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -833,120 +862,120 @@ L 518.4 43.2 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -959,10 +988,10 @@ L 274.909091 388.8 L 274.909091 231.709091 L 72 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + @@ -1016,17 +1045,17 @@ L 274.909091 231.709091 - + - + - + @@ -1034,17 +1063,17 @@ L 274.909091 231.709091 - + - + - + @@ -1052,17 +1081,17 @@ L 274.909091 231.709091 - + - + - + @@ -1070,17 +1099,17 @@ L 274.909091 231.709091 - + - + - + @@ -1088,17 +1117,17 @@ L 274.909091 231.709091 - + - + - + @@ -1106,17 +1135,17 @@ L 274.909091 231.709091 - + - + - + @@ -1124,17 +1153,17 @@ L 274.909091 231.709091 - + - + - + @@ -1142,17 +1171,17 @@ L 274.909091 231.709091 - + - + - + @@ -1160,27 +1189,27 @@ L 274.909091 231.709091 - + - + - + - + - - + + @@ -1188,17 +1217,17 @@ L 274.909091 231.709091 - + - + - + @@ -1206,17 +1235,17 @@ L 274.909091 231.709091 - + - + - + @@ -1224,17 +1253,17 @@ L 274.909091 231.709091 - + - + - + @@ -1242,17 +1271,17 @@ L 274.909091 231.709091 - + - + - + @@ -1260,17 +1289,17 @@ L 274.909091 231.709091 - + - + - + @@ -1278,17 +1307,17 @@ L 274.909091 231.709091 - + - + - + @@ -1296,17 +1325,17 @@ L 274.909091 231.709091 - + - + - + @@ -1314,17 +1343,17 @@ L 274.909091 231.709091 - + - + - + @@ -1332,17 +1361,17 @@ L 274.909091 231.709091 - + - + - + @@ -1350,27 +1379,27 @@ L 274.909091 231.709091 - + - + - + - + - - + + @@ -1382,10 +1411,10 @@ L 518.4 388.8 L 518.4 231.709091 L 315.490909 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + @@ -1439,197 +1468,198 @@ L 518.4 231.709091 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + @@ -1637,120 +1667,120 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1758,17 +1788,17 @@ z - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg b/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg index e62797eb4c23..343b5c0bd3d7 100644 --- a/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg +++ b/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-09T01:10:40.151601 + image/svg+xml + + + Matplotlib v0.1.0.dev50524+g1791319.d20240709, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,802 +35,853 @@ L 518.4 388.8 L 518.4 43.2 L 72 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"/> - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_text/multiline.svg b/lib/matplotlib/tests/baseline_images/test_text/multiline.svg index 15bfc30bebdc..598c1d92d1c1 100644 --- a/lib/matplotlib/tests/baseline_images/test_text/multiline.svg +++ b/lib/matplotlib/tests/baseline_images/test_text/multiline.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-09T01:10:40.770401 + image/svg+xml + + + Matplotlib v0.1.0.dev50524+g1791319.d20240709, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,481 +35,502 @@ L 518.4 388.8 L 518.4 43.2 L 72 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"/> - - + + - - - +" transform="scale(0.015625)"/> + + - - - - - + + + + + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - - - - + + + + + - + - - - - - + + + + + - - - - - + + + + + - + - - - - - + + + + + - + - - - - - + + + + + - - - - - - - - + + + + + + + + - + - - - - - + + + + + - - - + + + - - + - + + - - - - +" transform="scale(0.015625)"/> + + + - - - - - - - - + + + + + + + + - - + + + + - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg index c68cafc5e9c7..2075eca4868f 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:17.981141 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,288 +35,302 @@ L 271.943437 177.879375 L 271.943437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p471c10e632)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - - + + - + - + - + - - + - - +M 1159 2969 +Q 1341 3281 1617 3432 +Q 1894 3584 2278 3584 +Q 2916 3584 3314 3078 +Q 3713 2572 3713 1747 +Q 3713 922 3314 415 +Q 2916 -91 2278 -91 +Q 1894 -91 1617 61 +Q 1341 213 1159 525 +L 1159 0 +L 581 0 +L 581 4863 +L 1159 4863 +L 1159 2969 +z +" transform="scale(0.015625)"/> + + - - - - - - + + + + + + @@ -313,180 +338,186 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - + + + + + + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - - - + + + + @@ -497,104 +528,104 @@ L 553.463437 177.879375 L 553.463437 26.8475 L 333.953437 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p4ddb23cc8e)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -602,84 +633,84 @@ L 553.463437 26.8475 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -690,104 +721,104 @@ L 271.943437 387.399375 L 271.943437 236.3675 L 52.433438 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7d460e1d1c)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -795,84 +826,84 @@ L 271.943437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -883,104 +914,104 @@ L 553.463437 387.399375 L 553.463437 236.3675 L 333.953437 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p6f8bf01143)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -988,100 +1019,100 @@ L 553.463437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg index 88e6a404ac25..d7d66b644771 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:18.249876 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,288 +35,302 @@ L 271.943437 177.879375 L 271.943437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p471c10e632)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - - + + - + - + - + - - + - - +M 1159 2969 +Q 1341 3281 1617 3432 +Q 1894 3584 2278 3584 +Q 2916 3584 3314 3078 +Q 3713 2572 3713 1747 +Q 3713 922 3314 415 +Q 2916 -91 2278 -91 +Q 1894 -91 1617 61 +Q 1341 213 1159 525 +L 1159 0 +L 581 0 +L 581 4863 +L 1159 4863 +L 1159 2969 +z +" transform="scale(0.015625)"/> + + - - - - - - + + + + + + @@ -313,180 +338,186 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - + + + + + + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - - - + + + + @@ -497,104 +528,104 @@ L 271.943437 387.399375 L 271.943437 236.3675 L 52.433438 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7d460e1d1c)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -602,84 +633,84 @@ L 271.943437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -690,104 +721,104 @@ L 553.463437 387.399375 L 553.463437 26.8475 L 333.953437 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7f81023593)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -795,97 +826,97 @@ L 553.463437 26.8475 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg index de5e5efd5a1a..27f30e5a363b 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:18.594236 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,293 +35,302 @@ L 178.103437 108.039375 L 178.103437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p2cd46811c6)" style="fill: none; stroke: #0000ff; 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="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - - - - +" transform="scale(0.015625)"/> + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - - + + - + - + - + - + - + - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + @@ -318,289 +338,294 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + - + - + - - - - + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - - + + - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + - - + + - + - + - - - - - - - +" transform="scale(0.015625)"/> + + + + + + +" style="fill: #ffffff"/> - + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + +L 553.463438 108.039375 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +L 553.463438 26.8475 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -608,192 +633,192 @@ L 553.463437 26.8475 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + +" style="fill: #ffffff"/> - + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + +L 365.783438 387.399375 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +L 365.783438 166.5275 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -801,192 +826,192 @@ L 365.783437 166.5275 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - +" style="fill: #ffffff"/> - + - + - + - + - + - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -994,100 +1019,100 @@ L 553.463437 166.5275 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/meson.build b/lib/matplotlib/tests/meson.build index 148a40f11a2f..e4a00e850f7f 100644 --- a/lib/matplotlib/tests/meson.build +++ b/lib/matplotlib/tests/meson.build @@ -2,8 +2,8 @@ python_sources = [ '__init__.py', 'conftest.py', 'test_afm.py', - 'test_agg_filter.py', 'test_agg.py', + 'test_agg_filter.py', 'test_animation.py', 'test_api.py', 'test_arrow_patches.py', @@ -13,20 +13,23 @@ python_sources = [ 'test_backend_bases.py', 'test_backend_cairo.py', 'test_backend_gtk3.py', + 'test_backend_inline.py', 'test_backend_macosx.py', 'test_backend_nbagg.py', 'test_backend_pdf.py', 'test_backend_pgf.py', 'test_backend_ps.py', 'test_backend_qt.py', - 'test_backends_interactive.py', + 'test_backend_registry.py', 'test_backend_svg.py', 'test_backend_template.py', 'test_backend_tk.py', 'test_backend_tools.py', 'test_backend_webagg.py', + 'test_backends_interactive.py', 'test_basic.py', 'test_bbox_tight.py', + 'test_bezier.py', 'test_category.py', 'test_cbook.py', 'test_collections.py', @@ -43,8 +46,8 @@ python_sources = [ 'test_doc.py', 'test_dviread.py', 'test_figure.py', - 'test_fontconfig_pattern.py', 'test_font_manager.py', + 'test_fontconfig_pattern.py', 'test_ft2font.py', 'test_getattr.py', 'test_gridspec.py', @@ -54,11 +57,12 @@ python_sources = [ 'test_marker.py', 'test_mathtext.py', 'test_matplotlib.py', + 'test_multivariate_colormaps.py', 'test_mlab.py', 'test_offsetbox.py', 'test_patches.py', - 'test_patheffects.py', 'test_path.py', + 'test_patheffects.py', 'test_pickle.py', 'test_png.py', 'test_polar.py', @@ -78,13 +82,12 @@ python_sources = [ 'test_table.py', 'test_testing.py', 'test_texmanager.py', - 'test_textpath.py', 'test_text.py', + 'test_textpath.py', 'test_ticker.py', 'test_tightlayout.py', 'test_transforms.py', 'test_triangulation.py', - 'test_ttconv.py', 'test_type1font.py', 'test_units.py', 'test_usetex.py', diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 6ca74ed400b1..59387793605a 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import assert_array_almost_equal -from PIL import Image, TiffTags +from PIL import features, Image, TiffTags import pytest @@ -181,8 +181,8 @@ def process_image(self, padded_src, dpi): shadow.update_from(line) # offset transform - transform = mtransforms.offset_copy(line.get_transform(), ax.figure, - x=4.0, y=-6.0, units='points') + transform = mtransforms.offset_copy( + line.get_transform(), fig, x=4.0, y=-6.0, units='points') shadow.set_transform(transform) # adjust zorder of the shadow lines so that it is drawn below the @@ -199,7 +199,7 @@ def process_image(self, padded_src, dpi): def test_too_large_image(): - fig = plt.figure(figsize=(300, 1000)) + fig = plt.figure(figsize=(300, 2**25)) buff = io.BytesIO() with pytest.raises(ValueError): fig.savefig(buff) @@ -249,6 +249,7 @@ def test_pil_kwargs_tiff(): assert tags["ImageDescription"] == "test image" +@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available") def test_pil_kwargs_webp(): plt.plot([0, 1, 2], [0, 1, 0]) buf_small = io.BytesIO() @@ -262,6 +263,7 @@ def test_pil_kwargs_webp(): assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes +@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available") def test_webp_alpha(): plt.plot([0, 1, 2], [0, 1, 0]) buf = io.BytesIO() diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index 8b0f1e70114e..f04604c14cce 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Callable import re import typing -from typing import Any, Callable, TypeVar +from typing import Any, TypeVar import numpy as np import pytest @@ -48,6 +49,41 @@ def f(cls: Self) -> None: a.f +def test_warn_deprecated(): + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'The foo class was deprecated in Matplotlib 3\.10 and ' + r'will be removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo', obj_type='class') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. Use bar instead\.'): + _api.warn_deprecated('3.10', name='foo', alternative='bar') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. More information\.'): + _api.warn_deprecated('3.10', name='foo', addendum='More information.') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 4\.0\.'): + _api.warn_deprecated('3.10', name='foo', removal='4.0') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10\.'): + _api.warn_deprecated('3.10', name='foo', removal=False) + with pytest.warns(PendingDeprecationWarning, + match=r'foo will be deprecated in a future version'): + _api.warn_deprecated('3.10', name='foo', pending=True) + with pytest.raises(ValueError, match=r'cannot have a scheduled removal'): + _api.warn_deprecated('3.10', name='foo', pending=True, removal='3.12') + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=r'Complete replacement'): + _api.warn_deprecated('3.10', message='Complete replacement', name='foo', + alternative='bar', addendum='More information.', + obj_type='class', removal='4.0') + + def test_deprecate_privatize_attribute() -> None: class C: def __init__(self) -> None: self._attr = 1 diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index dbb5dd2305e0..e75572d776eb 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -208,7 +208,7 @@ def test_remove(): for art in [im, ln]: assert art.axes is None - assert art.figure is None + assert art.get_figure() is None assert im not in ax._mouseover_set assert fig.stale @@ -562,3 +562,37 @@ def draw(self, renderer, extra): assert 'aardvark' == art.draw(renderer, 'aardvark') assert 'aardvark' == art.draw(renderer, extra='aardvark') + + +def test_get_figure(): + fig = plt.figure() + sfig1 = fig.subfigures() + sfig2 = sfig1.subfigures() + ax = sfig2.subplots() + + assert fig.get_figure(root=True) is fig + assert fig.get_figure(root=False) is fig + + assert ax.get_figure() is sfig2 + assert ax.get_figure(root=False) is sfig2 + assert ax.get_figure(root=True) is fig + + # SubFigure.get_figure has separate implementation but should give consistent + # results to other artists. + assert sfig2.get_figure(root=False) is sfig1 + assert sfig2.get_figure(root=True) is fig + # Currently different results by default. + with pytest.warns(mpl.MatplotlibDeprecationWarning): + assert sfig2.get_figure() is fig + # No deprecation warning if root and parent figure are the same. + assert sfig1.get_figure() is fig + + # An artist not yet attached to anything has no figure. + ln = mlines.Line2D([], []) + assert ln.get_figure(root=True) is None + assert ln.get_figure(root=False) is None + + # figure attribute is root for (Sub)Figures but parent for other artists. + assert ax.figure is sfig2 + assert fig.figure is fig + assert sfig2.figure is fig diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e99ef129eb9a..e9218bc82573 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1,12 +1,14 @@ import contextlib -from collections import namedtuple +from collections import namedtuple, deque import datetime from decimal import Decimal from functools import partial +import gc import inspect import io from itertools import product import platform +import sys from types import SimpleNamespace import dateutil.tz @@ -23,6 +25,8 @@ import matplotlib.dates as mdates from matplotlib.figure import Figure from matplotlib.axes import Axes +from matplotlib.lines import Line2D +from matplotlib.collections import PathCollection import matplotlib.font_manager as mfont_manager import matplotlib.markers as mmarkers import matplotlib.patches as mpatches @@ -33,7 +37,7 @@ import matplotlib.text as mtext import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms -import mpl_toolkits.axisartist as AA # type: ignore +import mpl_toolkits.axisartist as AA # type: ignore[import] from numpy.testing import ( assert_allclose, assert_array_equal, assert_array_almost_equal) from matplotlib.testing.decorators import ( @@ -136,20 +140,20 @@ def test_label_shift(): # Test label re-centering on x-axis ax.set_xlabel("Test label", loc="left") ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" ax.set_xlabel("Test label", loc="right") - assert ax.xaxis.get_label().get_horizontalalignment() == "right" + assert ax.xaxis.label.get_horizontalalignment() == "right" ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" # Test label re-centering on y-axis ax.set_ylabel("Test label", loc="top") ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" ax.set_ylabel("Test label", loc="bottom") - assert ax.yaxis.get_label().get_horizontalalignment() == "left" + assert ax.yaxis.label.get_horizontalalignment() == "left" ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" @check_figures_equal(extensions=["png"]) @@ -1007,6 +1011,15 @@ def test_hexbin_bad_extents(): ax.hexbin(x, y, extent=(0, 1, 1, 0)) +def test_hexbin_string_norm(): + fig, ax = plt.subplots() + hex = ax.hexbin(np.random.rand(10), np.random.rand(10), norm="log", vmin=2, vmax=5) + assert isinstance(hex, matplotlib.collections.PolyCollection) + assert isinstance(hex.norm, matplotlib.colors.LogNorm) + assert hex.norm.vmin == 2 + assert hex.norm.vmax == 5 + + @image_comparison(['hexbin_empty.png'], remove_text=True) def test_hexbin_empty(): # From #3886: creating hexbin from empty dataset raises ValueError @@ -1049,6 +1062,27 @@ def test_hexbin_log(): marginals=True, reduce_C_function=np.sum) plt.colorbar(h) + # Make sure offsets are set + assert h.get_offsets().shape == (11558, 2) + + +def test_hexbin_log_offsets(): + x = np.geomspace(1, 100, 500) + + fig, ax = plt.subplots() + h = ax.hexbin(x, x, xscale='log', yscale='log', gridsize=2) + np.testing.assert_almost_equal( + h.get_offsets(), + np.array( + [[0, 0], + [0, 2], + [1, 0], + [1, 2], + [2, 0], + [2, 2], + [0.5, 1], + [1.5, 1]])) + @image_comparison(["hexbin_linear.png"], style="mpl20", remove_text=True) def test_hexbin_linear(): @@ -1465,6 +1499,20 @@ def test_pcolormesh_rgba(fig_test, fig_ref, dims, alpha): ax.pcolormesh(c[..., 0], cmap="gray", vmin=0, vmax=1, alpha=alpha) +@check_figures_equal(extensions=["png"]) +def test_pcolormesh_nearest_noargs(fig_test, fig_ref): + x = np.arange(4) + y = np.arange(7) + X, Y = np.meshgrid(x, y) + C = X + Y + + ax = fig_test.subplots() + ax.pcolormesh(C, shading="nearest") + + ax = fig_ref.subplots() + ax.pcolormesh(x, y, C, shading="nearest") + + @image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20') def test_pcolormesh_datetime_axis(): # Remove this line when this test image is regenerated. @@ -1541,7 +1589,9 @@ def test_pcolorargs(): x = np.ma.array(x, mask=(x < 0)) with pytest.raises(ValueError): ax.pcolormesh(x, y, Z[:-1, :-1]) - # Expect a warning with non-increasing coordinates + # If the X or Y coords do not possess monotonicity in their respective + # directions, a warning indicating a bad grid will be triggered. + # The case of specifying coordinates by inputting 1D arrays. x = [359, 0, 1] y = [-10, 10] X, Y = np.meshgrid(x, y) @@ -1549,6 +1599,27 @@ def test_pcolorargs(): with pytest.warns(UserWarning, match='are not monotonically increasing or decreasing'): ax.pcolormesh(X, Y, Z, shading='auto') + # The case of specifying coordinates by inputting 2D arrays. + x = np.linspace(-1, 1, 3) + y = np.linspace(-1, 1, 3) + X, Y = np.meshgrid(x, y) + Z = np.zeros(X.shape) + np.random.seed(19680801) + noise_X = np.random.random(X.shape) + noise_Y = np.random.random(Y.shape) + with pytest.warns(UserWarning, + match='are not monotonically increasing or ' + 'decreasing') as record: + # Small perturbations in coordinates will not disrupt the monotonicity + # of the X-coords and Y-coords in their respective directions. + # Therefore, no warnings will be triggered. + ax.pcolormesh(X+noise_X, Y+noise_Y, Z, shading='auto') + assert len(record) == 0 + # Large perturbations have disrupted the monotonicity of the X-coords + # and Y-coords in their respective directions, thus resulting in two + # bad grid warnings. + ax.pcolormesh(X+10*noise_X, Y+10*noise_Y, Z, shading='auto') + assert len(record) == 2 def test_pcolormesh_underflow_error(): @@ -2371,6 +2442,18 @@ def test_hist_zorder(histtype, zorder): assert patch.get_zorder() == zorder +def test_stairs_no_baseline_fill_warns(): + fig, ax = plt.subplots() + with pytest.warns(UserWarning, match="baseline=None and fill=True"): + ax.stairs( + [4, 5, 1, 0, 2], + [1, 2, 3, 4, 5, 6], + facecolor="blue", + baseline=None, + fill=True + ) + + @check_figures_equal(extensions=['png']) def test_stairs(fig_test, fig_ref): import matplotlib.lines as mlines @@ -2466,16 +2549,17 @@ def test_stairs_update(fig_test, fig_ref): @check_figures_equal(extensions=['png']) -def test_stairs_baseline_0(fig_test, fig_ref): - # Test - test_ax = fig_test.add_subplot() - test_ax.stairs([5, 6, 7], baseline=None) +def test_stairs_baseline_None(fig_test, fig_ref): + x = np.array([0, 2, 3, 5, 10]) + y = np.array([1.148, 1.231, 1.248, 1.25]) + + test_axes = fig_test.add_subplot() + test_axes.stairs(y, x, baseline=None) - # Ref - ref_ax = fig_ref.add_subplot() style = {'solid_joinstyle': 'miter', 'solid_capstyle': 'butt'} - ref_ax.plot(range(4), [5, 6, 7, 7], drawstyle='steps-post', **style) - ref_ax.set_ylim(0, None) + + ref_axes = fig_ref.add_subplot() + ref_axes.plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) def test_stairs_empty(): @@ -2933,7 +3017,7 @@ def test_scatter_singular_plural_arguments(self): def _params(c=None, xsize=2, *, edgecolors=None, **kwargs): - return (c, edgecolors, kwargs if kwargs is not None else {}, xsize) + return (c, edgecolors, kwargs, xsize) _result = namedtuple('_result', 'c, colors') @@ -3043,10 +3127,10 @@ def test_log_scales(): ax.set_yscale('log', base=5.5) ax.invert_yaxis() ax.set_xscale('log', base=9.0) - xticks, yticks = [ + xticks, yticks = ( [(t.get_loc(), t.label1.get_text()) for t in axis._update_ticks()] for axis in [ax.xaxis, ax.yaxis] - ] + ) assert xticks == [ (1.0, '$\\mathdefault{9^{0}}$'), (9.0, '$\\mathdefault{9^{1}}$'), @@ -3172,7 +3256,7 @@ def _bxp_test_helper( logstats = mpl.cbook.boxplot_stats( np.random.lognormal(mean=1.25, sigma=1., size=(37, 4)), **stats_kwargs) fig, ax = plt.subplots() - if bxp_kwargs.get('vert', True): + if bxp_kwargs.get('orientation', 'vertical') == 'vertical': ax.set_yscale('log') else: ax.set_xscale('log') @@ -3223,7 +3307,7 @@ def transform(stats): style='default', tol=0.1) def test_bxp_horizontal(): - _bxp_test_helper(bxp_kwargs=dict(vert=False)) + _bxp_test_helper(bxp_kwargs=dict(orientation='horizontal')) @image_comparison(['bxp_with_ylabels.png'], @@ -3236,7 +3320,8 @@ def transform(stats): s['label'] = label return stats - _bxp_test_helper(transform_stats=transform, bxp_kwargs=dict(vert=False)) + _bxp_test_helper(transform_stats=transform, + bxp_kwargs=dict(orientation='horizontal')) @image_comparison(['bxp_patchartist.png'], @@ -3565,7 +3650,6 @@ def test_boxplot_rc_parameters(): } rc_axis1 = { - 'boxplot.vertical': False, 'boxplot.whiskers': [0, 100], 'boxplot.patchartist': True, } @@ -3764,7 +3848,7 @@ def test_horiz_violinplot_baseline(): # First 9 digits of frac(sqrt(19)) np.random.seed(358898943) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=False, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, showextrema=False, showmedians=False) @@ -3774,7 +3858,7 @@ def test_horiz_violinplot_showmedians(): # First 9 digits of frac(sqrt(23)) np.random.seed(795831523) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=False, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, showextrema=False, showmedians=True) @@ -3784,7 +3868,7 @@ def test_horiz_violinplot_showmeans(): # First 9 digits of frac(sqrt(29)) np.random.seed(385164807) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=True, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=True, showextrema=False, showmedians=False) @@ -3794,7 +3878,7 @@ def test_horiz_violinplot_showextrema(): # First 9 digits of frac(sqrt(31)) np.random.seed(567764362) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=False, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, showextrema=True, showmedians=False) @@ -3804,7 +3888,7 @@ def test_horiz_violinplot_showall(): # First 9 digits of frac(sqrt(37)) np.random.seed(82762530) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=True, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=True, showextrema=True, showmedians=True, quantiles=[[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.4, 0.6]]) @@ -3815,7 +3899,7 @@ def test_horiz_violinplot_custompoints_10(): # First 9 digits of frac(sqrt(41)) np.random.seed(403124237) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=False, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, showextrema=False, showmedians=False, points=10) @@ -3825,7 +3909,7 @@ def test_horiz_violinplot_custompoints_200(): # First 9 digits of frac(sqrt(43)) np.random.seed(557438524) data = [np.random.normal(size=100) for _ in range(4)] - ax.violinplot(data, positions=range(4), vert=False, showmeans=False, + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, showextrema=False, showmedians=False, points=200) @@ -3836,11 +3920,11 @@ def test_violinplot_sides(): data = [np.random.normal(size=100)] # Check horizontal violinplot for pos, side in zip([0, -0.5, 0.5], ['both', 'low', 'high']): - ax.violinplot(data, positions=[pos], vert=False, showmeans=False, + ax.violinplot(data, positions=[pos], orientation='horizontal', showmeans=False, showextrema=True, showmedians=True, side=side) # Check vertical violinplot for pos, side in zip([4, 3.5, 4.5], ['both', 'low', 'high']): - ax.violinplot(data, positions=[pos], vert=True, showmeans=False, + ax.violinplot(data, positions=[pos], orientation='vertical', showmeans=False, showextrema=True, showmedians=True, side=side) @@ -4589,6 +4673,64 @@ def test_hist_stacked_bar(): ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) +@pytest.mark.parametrize('kwargs', ({'facecolor': ["b", "g", "r"]}, + {'edgecolor': ["b", "g", "r"]}, + {'hatch': ["/", "\\", "."]}, + {'linestyle': ["-", "--", ":"]}, + {'linewidth': [1, 1.5, 2]}, + {'color': ["b", "g", "r"]})) +@check_figures_equal(extensions=["png"]) +def test_hist_vectorized_params(fig_test, fig_ref, kwargs): + np.random.seed(19680801) + xs = [np.random.randn(n) for n in [20, 50, 100]] + + (axt1, axt2) = fig_test.subplots(2) + (axr1, axr2) = fig_ref.subplots(2) + + for histtype, axt, axr in [("stepfilled", axt1, axr1), ("step", axt2, axr2)]: + _, bins, _ = axt.hist(xs, bins=10, histtype=histtype, **kwargs) + + kw, values = next(iter(kwargs.items())) + for i, (x, value) in enumerate(zip(xs, values)): + axr.hist(x, bins=bins, histtype=histtype, **{kw: value}, + zorder=(len(xs)-i)/2) + + +@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 + # When the expected edgecolor is 'k'(black), + # it corresponds to the patch.edgecolor rcParam + [({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'edgecolor': 'g'}, 'r', 'g'), + ({'histtype': 'step', 'color': 'r', + 'edgecolor': 'g'}, ('r', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y'}, ('y', 0), 'r'), + ({'histtype': 'stepfilled', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'facecolor': 'y', + 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r'}, 'r', 'k'), + ({'histtype': 'step', 'color': 'r'}, ('r', 0), 'r'), + ({'histtype': 'stepfilled', 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'facecolor': 'y'}, ('y', 0), 'C0'), + ({'histtype': 'stepfilled', 'edgecolor': 'g'}, 'C0', 'g'), + ({'histtype': 'step', 'edgecolor': 'g'}, ('C0', 0), 'g'), + ({'histtype': 'stepfilled'}, 'C0', 'k'), + ({'histtype': 'step'}, ('C0', 0), 'C0')]) +def test_hist_color_semantics(kwargs, patch_face, patch_edge): + _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) + assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], + [patch_face, patch_edge]) for p in patches) + + def test_hist_barstacked_bottom_unchanged(): b = np.array([10, 20]) plt.hist([[0, 1], [0, 1]], 2, histtype="barstacked", bottom=b) @@ -4600,6 +4742,15 @@ def test_hist_emptydata(): ax.hist([[], range(10), range(10)], histtype="step") +def test_hist_unused_labels(): + # When a list with one dataset and N elements is provided and N labels, ensure + # that the first label is used for the dataset and all other labels are ignored + fig, ax = plt.subplots() + ax.hist([[1, 2, 3]], label=["values", "unused", "also unused"]) + _, labels = ax.get_legend_handles_labels() + assert labels == ["values"] + + def test_hist_labels(): # test singleton labels OK fig, ax = plt.subplots() @@ -6039,6 +6190,27 @@ def test_pie_get_negative_values(): ax.pie([5, 5, -3], explode=[0, .1, .2]) +def test_pie_invalid_explode(): + # Test ValueError raised when feeding short explode list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], explode=[0.1, 0.1]) + + +def test_pie_invalid_labels(): + # Test ValueError raised when feeding short labels list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], labels=["One", "Two"]) + + +def test_pie_invalid_radius(): + # Test ValueError raised when feeding negative radius to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], radius=-5) + + def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] @@ -6205,7 +6377,7 @@ def formatter_func(x, pos): ax.set_xticks([-1, 0, 1, 2, 3]) ax.set_xlim(-0.5, 2.5) - ax.figure.canvas.draw() + fig.canvas.draw() tick_texts = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] assert tick_texts == ["", "", "unit value", "", ""] @@ -6516,6 +6688,27 @@ def test_pcolorfast_bad_dims(): ax.pcolorfast(np.empty(6), np.empty((4, 7)), np.empty((8, 8))) +def test_pcolorfast_regular_xy_incompatible_size(): + """ + Test that the sizes of X, Y, C are compatible for regularly spaced X, Y. + + Note that after the regualar-spacing check, pcolorfast may go into the + fast "image" mode, where the individual X, Y positions are not used anymore. + Therefore, the algorithm had worked with any regularly number of regularly + spaced values, but discarded their values. + """ + fig, ax = plt.subplots() + with pytest.raises( + ValueError, match=r"Length of X \(5\) must be one larger than the " + r"number of columns in C \(20\)"): + ax.pcolorfast(np.arange(5), np.arange(11), np.random.rand(10, 20)) + + with pytest.raises( + ValueError, match=r"Length of Y \(5\) must be one larger than the " + r"number of rows in C \(10\)"): + ax.pcolorfast(np.arange(21), np.arange(5), np.random.rand(10, 20)) + + def test_shared_scale(): fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) @@ -7126,6 +7319,18 @@ def test_title_no_move_off_page(): assert tt.get_position()[1] == 1.0 +def test_title_inset_ax(): + # Title should be above any child axes + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.set_title('Title') + fig.draw_without_rendering() + assert ax.title.get_position()[1] == 1 + ax.inset_axes([0, 1, 1, 0.1]) + fig.draw_without_rendering() + assert ax.title.get_position()[1] == 1.1 + + def test_offset_label_color(): # Tests issue 6440 fig, ax = plt.subplots() @@ -7391,12 +7596,12 @@ def test_inset(): rect = [xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]] - rec, connectors = ax.indicate_inset(bounds=rect) - assert connectors is None + inset = ax.indicate_inset(bounds=rect) + assert inset.connectors is None fig.canvas.draw() xx = np.array([[1.5, 2.], [2.15, 2.5]]) - assert np.all(rec.get_bbox().get_points() == xx) + assert np.all(inset.rectangle.get_bbox().get_points() == xx) def test_zoom_inset(): @@ -7420,9 +7625,10 @@ def test_zoom_inset(): axin1.set_ylim([2, 2.5]) axin1.set_aspect(ax.get_aspect()) - rec, connectors = ax.indicate_inset_zoom(axin1) - assert len(connectors) == 4 + with pytest.warns(mpl.MatplotlibDeprecationWarning): + rec, connectors = ax.indicate_inset_zoom(axin1) fig.canvas.draw() + assert len(connectors) == 4 xx = np.array([[1.5, 2.], [2.15, 2.5]]) assert np.all(rec.get_bbox().get_points() == xx) @@ -7472,8 +7678,8 @@ def test_indicate_inset_inverted(x_inverted, y_inverted): if y_inverted: ax1.invert_yaxis() - rect, bounds = ax1.indicate_inset([2, 2, 5, 4], ax2) - lower_left, upper_left, lower_right, upper_right = bounds + inset = ax1.indicate_inset([2, 2, 5, 4], ax2) + lower_left, upper_left, lower_right, upper_right = inset.connectors sign_x = -1 if x_inverted else 1 sign_y = -1 if y_inverted else 1 @@ -8298,7 +8504,7 @@ def test_ylabel_ha_with_position(ha): ax = fig.subplots() ax.set_ylabel("test", y=1, ha=ha) ax.yaxis.set_label_position("right") - assert ax.yaxis.get_label().get_ha() == ha + assert ax.yaxis.label.get_ha() == ha def test_bar_label_location_vertical(): @@ -8823,11 +9029,11 @@ def test_cla_clears_children_axes_and_fig(): img = ax.imshow([[1]]) for art in lines + [img]: assert art.axes is ax - assert art.figure is fig + assert art.get_figure() is fig ax.clear() for art in lines + [img]: assert art.axes is None - assert art.figure is None + assert art.get_figure() is None def test_child_axes_removal(): @@ -9044,6 +9250,49 @@ def test_axes_clear_behavior(fig_ref, fig_test, which): ax_test.grid(True) +@pytest.mark.skipif( + sys.version_info[:3] == (3, 13, 0) and sys.version_info.releaselevel != "final", + reason="https://github.com/python/cpython/issues/124538", +) +def test_axes_clear_reference_cycle(): + def assert_not_in_reference_cycle(start): + # Breadth first search. Return True if we encounter the starting node + to_visit = deque([start]) + explored = set() + while len(to_visit) > 0: + parent = to_visit.popleft() + for child in gc.get_referents(parent): + if id(child) in explored: + continue + assert child is not start + explored.add(id(child)) + to_visit.append(child) + + fig = Figure() + ax = fig.add_subplot() + points = np.random.rand(1000) + ax.plot(points, points) + ax.scatter(points, points) + ax_children = ax.get_children() + fig.clear() # This should break the reference cycle + + # Care most about the objects that scale with number of points + big_artists = [ + a for a in ax_children + if isinstance(a, (Line2D, PathCollection)) + ] + assert len(big_artists) > 0 + for big_artist in big_artists: + assert_not_in_reference_cycle(big_artist) + assert len(ax_children) > 0 + for child in ax_children: + # Make sure this doesn't raise because the child is already removed. + try: + child.remove() + except NotImplementedError: + pass # not implemented is expected for some artists + + def test_boxplot_tick_labels(): # Test the renamed `tick_labels` parameter. # Test for deprecation of old name `labels`. @@ -9073,3 +9322,194 @@ def test_latex_pie_percent(fig_test, fig_ref): ax1 = fig_ref.subplots() ax1.pie(data, autopct=r"%1.0f\%%", textprops={'usetex': True}) + + +@check_figures_equal(extensions=['png']) +def test_violinplot_orientation(fig_test, fig_ref): + # Test the `orientation : {'vertical', 'horizontal'}` + # parameter and deprecation of `vert: bool`. + fig, axs = plt.subplots(nrows=1, ncols=3) + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + axs[0].violinplot(all_data) # Default vertical plot. + # xticks and yticks should be at their default position. + assert all(axs[0].get_xticks() == np.array( + [0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5])) + assert all(axs[0].get_yticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + + # Horizontal plot using new `orientation` keyword. + axs[1].violinplot(all_data, orientation='horizontal') + # xticks and yticks should be swapped. + assert all(axs[1].get_xticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + assert all(axs[1].get_yticks() == np.array( + [0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5])) + + plt.close() + + # Compare images between a figure that + # uses vert and one that uses orientation. + ax_ref = fig_ref.subplots() + + with pytest.warns(PendingDeprecationWarning, match='vert: bool'): + ax_ref.violinplot(all_data, vert=False) + + ax_test = fig_test.subplots() + ax_test.violinplot(all_data, orientation='horizontal') + + +@check_figures_equal(extensions=['png']) +def test_boxplot_orientation(fig_test, fig_ref): + # Test the `orientation : {'vertical', 'horizontal'}` + # parameter and deprecation of `vert: bool`. + fig, axs = plt.subplots(nrows=1, ncols=2) + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + axs[0].boxplot(all_data) # Default vertical plot. + # xticks and yticks should be at their default position. + assert all(axs[0].get_xticks() == np.array( + [1, 2, 3, 4])) + assert all(axs[0].get_yticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + + # Horizontal plot using new `orientation` keyword. + axs[1].boxplot(all_data, orientation='horizontal') + # xticks and yticks should be swapped. + assert all(axs[1].get_xticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + assert all(axs[1].get_yticks() == np.array( + [1, 2, 3, 4])) + + plt.close() + + # Deprecation of `vert: bool` keyword and + # 'boxplot.vertical' rcparam. + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='was deprecated in Matplotlib 3.10'): + # Compare images between a figure that + # uses vert and one that uses orientation. + with mpl.rc_context({'boxplot.vertical': False}): + ax_ref = fig_ref.subplots() + ax_ref.boxplot(all_data) + + ax_test = fig_test.subplots() + ax_test.boxplot(all_data, orientation='horizontal') + + +@image_comparison(["use_colorizer_keyword.png"], + tol=0.05 if platform.machine() == 'arm64' else 0) +def test_use_colorizer_keyword(): + # test using the colorizer keyword + np.random.seed(0) + rand_x = np.random.random(100) + rand_y = np.random.random(100) + c = np.arange(25, dtype='float32').reshape((5, 5)) + + fig, axes = plt.subplots(3, 4) + norm = mpl.colors.Normalize(4, 20) + cl = mpl.colorizer.Colorizer(norm=norm, cmap='RdBu') + + axes[0, 0].scatter(c, c, c=c, colorizer=cl) + axes[0, 1].hexbin(rand_x, rand_y, colorizer=cl, gridsize=(2, 2)) + axes[0, 2].imshow(c, colorizer=cl) + axes[0, 3].pcolor(c, colorizer=cl) + axes[1, 0].pcolormesh(c, colorizer=cl) + axes[1, 1].pcolorfast(c, colorizer=cl) # style = image + axes[1, 2].pcolorfast((0, 1, 2, 3, 4, 5), (0, 1, 2, 3, 5, 6), c, + colorizer=cl) # style = pcolorimage + axes[1, 3].pcolorfast(c.T, c, c[:4, :4], colorizer=cl) # style = quadmesh + axes[2, 0].contour(c, colorizer=cl) + axes[2, 1].contourf(c, colorizer=cl) + axes[2, 2].tricontour(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl) + axes[2, 3].tricontourf(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl) + + fig.figimage(np.repeat(np.repeat(c, 15, axis=0), 15, axis=1), colorizer=cl) + remove_ticks_and_titles(fig) + + +def test_wrong_use_colorizer(): + # test using the colorizer keyword and norm or cmap + np.random.seed(0) + rand_x = np.random.random(100) + rand_y = np.random.random(100) + c = np.arange(25, dtype='float32').reshape((5, 5)) + + fig, axes = plt.subplots(3, 4) + norm = mpl.colors.Normalize(4, 20) + cl = mpl.colorizer.Colorizer(norm=norm, cmap='RdBu') + + match_str = "The `colorizer` keyword cannot be used simultaneously" + kwrds = [{'vmin': 0}, {'vmax': 0}, {'norm': 'log'}, {'cmap': 'viridis'}] + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 0].scatter(c, c, c=c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 0].scatter(c, c, c=c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 1].hexbin(rand_x, rand_y, colorizer=cl, gridsize=(2, 2), **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 2].imshow(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 3].pcolor(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 0].pcolormesh(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 1].pcolorfast(c, colorizer=cl, **kwrd) # style = image + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 2].pcolorfast((0, 1, 2, 3, 4, 5), (0, 1, 2, 3, 5, 6), c, + colorizer=cl, **kwrd) # style = pcolorimage + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 3].pcolorfast(c.T, c, c[:4, :4], colorizer=cl, **kwrd) # quadmesh + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 0].contour(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 1].contourf(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 2].tricontour(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl, + **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 3].tricontourf(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl, + **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + fig.figimage(c, colorizer=cl, **kwrd) + + +def test_bar_color_precedence(): + # Test the precedence of 'color' and 'facecolor' in bar plots + fig, ax = plt.subplots() + + # case 1: no color specified + bars = ax.bar([1, 2, 3], [4, 5, 6]) + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'blue') + + # case 2: Only 'color' + bars = ax.bar([11, 12, 13], [4, 5, 6], color='red') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'red') + + # case 3: Only 'facecolor' + bars = ax.bar([21, 22, 23], [4, 5, 6], facecolor='yellow') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'yellow') + + # case 4: 'facecolor' and 'color' + bars = ax.bar([31, 32, 33], [4, 5, 6], color='red', facecolor='green') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'green') diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index 97b5f88dede1..c1b09006607d 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -8,3 +8,33 @@ def test_tick_labelcolor_array(): # Smoke test that we can instantiate a Tick with labelcolor as array. ax = plt.axes() XTick(ax, 0, labelcolor=np.array([1, 0, 0, 1])) + + +def test_axis_not_in_layout(): + fig1, (ax1_left, ax1_right) = plt.subplots(ncols=2, layout='constrained') + fig2, (ax2_left, ax2_right) = plt.subplots(ncols=2, layout='constrained') + + # 100 label overlapping the end of the axis + ax1_left.set_xlim([0, 100]) + # 100 label not overlapping the end of the axis + ax2_left.set_xlim([0, 120]) + + for ax in ax1_left, ax2_left: + ax.set_xticks([0, 100]) + ax.xaxis.set_in_layout(False) + + for fig in fig1, fig2: + fig.draw_without_rendering() + + # Positions should not be affected by overlapping 100 label + assert ax1_left.get_position().bounds == ax2_left.get_position().bounds + assert ax1_right.get_position().bounds == ax2_right.get_position().bounds + + +def test_translate_tick_params_reverse(): + fig, ax = plt.subplots() + kw = {'label1On': 'a', 'label2On': 'b', 'tick1On': 'c', 'tick2On': 'd'} + assert (ax.xaxis._translate_tick_params(kw, reverse=True) == + {'labelbottom': 'a', 'labeltop': 'b', 'bottom': 'c', 'top': 'd'}) + assert (ax.yaxis._translate_tick_params(kw, reverse=True) == + {'labelleft': 'a', 'labelright': 'b', 'left': 'c', 'right': 'd'}) diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 3a49f0ec08ec..3e1f524ed1c9 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -283,10 +283,11 @@ def test_toolbar_zoompan(): with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): plt.rcParams['toolbar'] = 'toolmanager' ax = plt.gca() + fig = ax.get_figure() assert ax.get_navigate_mode() is None - ax.figure.canvas.manager.toolmanager.trigger_tool('zoom') + fig.canvas.manager.toolmanager.trigger_tool('zoom') assert ax.get_navigate_mode() == "ZOOM" - ax.figure.canvas.manager.toolmanager.trigger_tool('pan') + fig.canvas.manager.toolmanager.trigger_tool('pan') assert ax.get_navigate_mode() == "PAN" diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py index d7fa4329cfc8..b4c6e3d7fca8 100644 --- a/lib/matplotlib/tests/test_backend_gtk3.py +++ b/lib/matplotlib/tests/test_backend_gtk3.py @@ -1,13 +1,15 @@ +import os from matplotlib import pyplot as plt import pytest +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 + from gi.repository import Gdk, Gtk # type: ignore[import] fig = plt.figure() buf = [] @@ -46,3 +48,27 @@ def receive(event): 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 + fig, ax = plt.subplots() + ax.imshow([[1]]) + with mock.patch("gi.repository.Gtk.FileFilter") as fileFilter: + filt = fileFilter.return_value + filt.get_name.return_value = "Portable Network Graphics" + with mock.patch("gi.repository.Gtk.FileChooserDialog") as dialogChooser: + dialog = dialogChooser.return_value + dialog.get_filter.return_value = filt + dialog.get_filename.return_value = "foobar.png" + dialog.run.return_value = Gtk.ResponseType.OK + fname = fig.canvas.manager.toolbar.save_figure() + os.remove("foobar.png") + assert fname == "foobar.png" + + with mock.patch("gi.repository.Gtk.MessageDialog"): + dialog.get_filename.return_value = None + dialog.run.return_value = Gtk.ResponseType.OK + fname = fig.canvas.manager.toolbar.save_figure() + assert fname is None diff --git a/lib/matplotlib/tests/test_backend_inline.py b/lib/matplotlib/tests/test_backend_inline.py index 4112eb213e2c..6f0d67d51756 100644 --- a/lib/matplotlib/tests/test_backend_inline.py +++ b/lib/matplotlib/tests/test_backend_inline.py @@ -1,7 +1,6 @@ import os from pathlib import Path from tempfile import TemporaryDirectory -import sys import pytest @@ -13,7 +12,6 @@ pytest.importorskip('matplotlib_inline') -@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason="Requires Python 3.10+") def test_ipynb(): nb_path = Path(__file__).parent / 'test_inline_01.ipynb' diff --git a/lib/matplotlib/tests/test_backend_macosx.py b/lib/matplotlib/tests/test_backend_macosx.py index 7431481de8ae..8e50ddf84024 100644 --- a/lib/matplotlib/tests/test_backend_macosx.py +++ b/lib/matplotlib/tests/test_backend_macosx.py @@ -1,6 +1,7 @@ import os import pytest +from unittest import mock import matplotlib as mpl import matplotlib.pyplot as plt @@ -50,3 +51,17 @@ def new_choose_save_file(title, directory, filename): def test_ipython(): from matplotlib.testing import ipython_in_subprocess ipython_in_subprocess("osx", {(8, 24): "macosx", (7, 0): "MacOSX"}) + + +@pytest.mark.backend('macosx') +def test_save_figure_return(): + fig, ax = plt.subplots() + ax.imshow([[1]]) + prop = "matplotlib.backends._macosx.choose_save_file" + with mock.patch(prop, return_value="foobar.png"): + fname = fig.canvas.manager.toolbar.save_figure() + os.remove("foobar.png") + assert fname == "foobar.png" + with mock.patch(prop, return_value=None): + fname = fig.canvas.manager.toolbar.save_figure() + assert fname is None diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index ad565ea9e81b..c816c4715ae2 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -14,7 +14,7 @@ from matplotlib.cbook import _get_data_path from matplotlib.ft2font import FT2Font from matplotlib.font_manager import findfont, FontProperties -from matplotlib.backends._backend_pdf_ps import get_glyphs_subset +from matplotlib.backends._backend_pdf_ps import get_glyphs_subset, font_as_file from matplotlib.backends.backend_pdf import PdfPages from matplotlib.patches import Rectangle from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -81,48 +81,18 @@ def test_multipage_properfinalize(): def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - def test_composite_image(): # Test that figures can be saved with and without combining multiple images @@ -407,7 +377,8 @@ def test_glyphs_subset(): nosubfont.set_text(chars) # subsetted FT2Font - subfont = FT2Font(get_glyphs_subset(fpath, chars)) + with get_glyphs_subset(fpath, chars) as subset: + subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) nosubcmap = nosubfont.get_charmap() @@ -463,3 +434,15 @@ def test_otf_font_smoke(family_name, file_name): fig = plt.figure() fig.text(0.15, 0.475, "Привет мир!") fig.savefig(io.BytesIO(), format="pdf") + + +@image_comparison(["truetype-conversion.pdf"]) +# mpltest.ttf does not have "l"/"p" glyphs so we get a warning when trying to +# get the font extents. +def test_truetype_conversion(recwarn): + mpl.rcParams['pdf.fonttype'] = 3 + fig, ax = plt.subplots() + ax.text(0, 0, "ABCDE", + font=Path(__file__).with_name("mpltest.ttf"), fontsize=80) + ax.set_xticks([]) + ax.set_yticks([]) diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 8a83515f161c..e218a81cdceb 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -288,48 +288,18 @@ def test_pdf_pages_metadata_check(monkeypatch, system): @needs_pgf_xelatex def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - @needs_pgf_xelatex def test_tex_restart_after_error(): @@ -406,3 +376,27 @@ def test_sketch_params(): # \pgfdecoratecurrentpath must be after the path definition and before the # path is used (\pgfusepath) assert baseline in buf + + +# test to make sure that the document font size is set consistently (see #26892) +@needs_pgf_xelatex +@pytest.mark.skipif( + not _has_tex_package('unicode-math'), reason='needs unicode-math.sty' +) +@pytest.mark.backend('pgf') +@image_comparison(['pgf_document_font_size.pdf'], style='default', remove_text=True) +def test_document_font_size(): + mpl.rcParams.update({ + 'pgf.texsystem': 'xelatex', + 'pgf.rcfonts': False, + 'pgf.preamble': r'\usepackage{unicode-math}', + }) + plt.figure() + plt.plot([], + label=r'$this is a very very very long math label a \times b + 10^{-3}$ ' + r'and some text' + ) + plt.plot([], + label=r'\normalsize the document font size is \the\fontdimen6\font' + ) + plt.legend() diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index c587a00c0af9..cc968795802e 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -371,10 +371,10 @@ def test_colorbar_shift(tmp_path): plt.colorbar() -def test_auto_papersize_deprecation(): +def test_auto_papersize_removal(): fig = plt.figure() - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): fig.savefig(io.BytesIO(), format='eps', papertype='auto') - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): mpl.rcParams['ps.papersize'] = 'auto' diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 5eb1ea77554d..60f8a4f49bb8 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -15,7 +15,8 @@ from matplotlib import _c_internal_utils try: - from matplotlib.backends.qt_compat import QtGui, QtWidgets # type: ignore # noqa + 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 except ImportError: pytestmark = pytest.mark.skip('No usable Qt bindings') @@ -225,6 +226,20 @@ def test_figureoptions(): fig.canvas.manager.toolbar.edit_parameters() +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_save_figure_return(): + fig, ax = plt.subplots() + ax.imshow([[1]]) + prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName" + with mock.patch(prop, return_value=("foobar.png", None)): + fname = fig.canvas.manager.toolbar.save_figure() + os.remove("foobar.png") + assert fname == "foobar.png" + with mock.patch(prop, return_value=(None, None)): + fname = fig.canvas.manager.toolbar.save_figure() + assert fname is None + + @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_figureoptions_with_datetime_axes(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 01edbf870fb4..b850a9ab6ff5 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -10,6 +10,7 @@ import matplotlib as mpl from matplotlib.figure import Figure +from matplotlib.patches import Circle from matplotlib.text import Text import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -299,6 +300,33 @@ def include(gid, obj): assert gid in buf +def test_clip_path_ids_reuse(): + fig, circle = Figure(), Circle((0, 0), radius=10) + for i in range(5): + ax = fig.add_subplot() + aimg = ax.imshow([[i]]) + aimg.set_clip_path(circle) + + inner_circle = Circle((0, 0), radius=1) + ax = fig.add_subplot() + aimg = ax.imshow([[0]]) + aimg.set_clip_path(inner_circle) + + with BytesIO() as fd: + fig.savefig(fd, format='svg') + buf = fd.getvalue() + + tree = xml.etree.ElementTree.fromstring(buf) + ns = 'http://www.w3.org/2000/svg' + + clip_path_ids = set() + for node in tree.findall(f'.//{{{ns}}}clipPath[@id]'): + node_id = node.attrib['id'] + assert node_id not in clip_path_ids # assert ID uniqueness + clip_path_ids.add(node_id) + assert len(clip_path_ids) == 2 # only two clipPaths despite reuse in multiple axes + + def test_savefig_tight(): # Check that the draw-disabled renderer correctly disables open/close_group # as well. @@ -315,13 +343,17 @@ def test_url(): s.set_urls(['https://example.com/foo', 'https://example.com/bar', None]) # Line2D - p, = plt.plot([1, 3], [6, 5]) + p, = plt.plot([2, 3, 4], [4, 5, 6]) p.set_url('https://example.com/baz') + # Line2D markers-only + p, = plt.plot([3, 4, 5], [4, 5, 6], linestyle='none', marker='x') + p.set_url('https://example.com/quux') + b = BytesIO() fig.savefig(b, format='svg') b = b.getvalue() - for v in [b'foo', b'bar', b'baz']: + for v in [b'foo', b'bar', b'baz', b'quux']: assert b'https://example.com/' + v in b @@ -603,12 +635,13 @@ def test_svg_font_string(font_str, include_generic): text_count = 0 for text_element in tree.findall(f".//{{{ns}}}text"): text_count += 1 - font_info = dict( + font_style = dict( map(lambda x: x.strip(), _.strip().split(":")) for _ in dict(text_element.items())["style"].split(";") - )["font"] + ) - assert font_info == f"{size}px {font_str}" + assert font_style["font-size"] == f"{size}px" + assert font_style["font-family"] == font_str assert text_count == len(ax.texts) @@ -641,3 +674,34 @@ def test_annotationbbox_gid(): expected = '' assert expected in buf + + +def test_svgid(): + """Test that `svg.id` rcparam appears in output svg if not None.""" + + fig, ax = plt.subplots() + ax.plot([1, 2, 3], [3, 2, 1]) + fig.canvas.draw() + + # Default: svg.id = None + with BytesIO() as fd: + fig.savefig(fd, format='svg') + buf = fd.getvalue().decode() + + tree = xml.etree.ElementTree.fromstring(buf) + + assert plt.rcParams['svg.id'] is None + assert not tree.findall('.[@id]') + + # String: svg.id = str + svg_id = 'a test for issue 28535' + plt.rc('svg', id=svg_id) + + with BytesIO() as fd: + fig.savefig(fd, format='svg') + buf = fd.getvalue().decode() + + tree = xml.etree.ElementTree.fromstring(buf) + + assert plt.rcParams['svg.id'] == svg_id + assert tree.findall(f'.[@id="{svg_id}"]') diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 89782e8a66f3..1210c8c9993e 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -196,6 +196,23 @@ class Toolbar(NavigationToolbar2Tk): print("success") +@_isolated_tk_test(success_count=2) +def test_save_figure_return(): + import matplotlib.pyplot as plt + from unittest import mock + fig = plt.figure() + prop = "tkinter.filedialog.asksaveasfilename" + with mock.patch(prop, return_value="foobar.png"): + fname = fig.canvas.manager.toolbar.save_figure() + os.remove("foobar.png") + assert fname == "foobar.png" + print("success") + with mock.patch(prop, return_value=""): + fname = fig.canvas.manager.toolbar.save_figure() + assert fname is None + print("success") + + @_isolated_tk_test(success_count=1) def test_canvas_focus(): import tkinter as tk diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index ca702bc1d99c..063c72e2cde7 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -78,7 +78,10 @@ def _get_available_interactive_backends(): missing = [dep for dep in deps if not importlib.util.find_spec(dep)] if missing: reason = "{} cannot be imported".format(", ".join(missing)) - elif env["MPLBACKEND"] == "tkagg" and _is_linux_and_xdisplay_invalid: + elif _is_linux_and_xdisplay_invalid and ( + env["MPLBACKEND"] == "tkagg" + # Remove when https://github.com/wxWidgets/Phoenix/pull/2638 is out. + or env["MPLBACKEND"].startswith("wx")): reason = "$DISPLAY is unset" elif _is_linux_and_display_invalid: reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" @@ -86,7 +89,7 @@ def _get_available_interactive_backends(): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): try: - import gi # type: ignore + import gi # type: ignore[import] except ImportError: # Though we check that `gi` exists above, it is possible that its # C-level dependencies are not available, and then it still raises an @@ -170,7 +173,8 @@ def _test_interactive_impl(): if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. - plt.figure() + fig = plt.figure() + plt.close(fig) # Check that we cannot switch to a backend using another interactive # framework, but can switch to a backend using cairo instead of agg, @@ -228,10 +232,7 @@ def check_alt_backend(alt_backend): result_after = io.BytesIO() fig.savefig(result_after, format='png') - if not backend.startswith('qt5') and sys.platform == 'darwin': - # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS - # to not resize incorrectly. - assert result.getvalue() == result_after.getvalue() + assert result.getvalue() == result_after.getvalue() @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @@ -243,6 +244,9 @@ def test_interactive_backend(env, toolbar): pytest.skip("toolmanager is not implemented for macosx.") if env["MPLBACKEND"] == "wx": pytest.skip("wx backend is deprecated; tests failed on appveyor") + if env["MPLBACKEND"] == "wxagg" and toolbar == "toolmanager": + pytest.skip("Temporarily deactivated: show() changes figure height " + "and thus fails the test") try: proc = _run_helper( _test_interactive_impl, @@ -448,10 +452,12 @@ def qt5_and_qt6_pairs(): for qt5 in qt5_bindings: for qt6 in qt6_bindings: - for pair in ([qt5, qt6], [qt6, qt5]): - yield pair + yield from ([qt5, qt6], [qt6, qt5]) +@pytest.mark.skipif( + sys.platform == "linux" and not _c_internal_utils.display_is_valid(), + reason="$DISPLAY and $WAYLAND_DISPLAY are unset") @pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()]) def test_cross_Qt_imports(host, mpl): try: diff --git a/lib/matplotlib/tests/test_basic.py b/lib/matplotlib/tests/test_basic.py index 6bd417876857..f6aa1e458555 100644 --- a/lib/matplotlib/tests/test_basic.py +++ b/lib/matplotlib/tests/test_basic.py @@ -11,7 +11,7 @@ def test_simple(): def test_override_builtins(): - import pylab # type: ignore + import pylab # type: ignore[import] ok_to_override = { '__name__', '__doc__', diff --git a/lib/matplotlib/tests/test_category.py b/lib/matplotlib/tests/test_category.py index fd4aec88b574..b724e5839c4d 100644 --- a/lib/matplotlib/tests/test_category.py +++ b/lib/matplotlib/tests/test_category.py @@ -246,6 +246,14 @@ def test_update_plot(self, plotter): axis_test(ax.xaxis, ['a', 'b', 'd', 'c']) axis_test(ax.yaxis, ['e', 'g', 'f', 'a', 'b', 'd']) + def test_update_plot_heterogenous_plotter(self): + ax = plt.figure().subplots() + ax.scatter(['a', 'b'], ['e', 'g']) + ax.plot(['a', 'b', 'd'], ['f', 'a', 'b']) + ax.bar(['b', 'c', 'd'], ['g', 'e', 'd']) + axis_test(ax.xaxis, ['a', 'b', 'd', 'c']) + axis_test(ax.yaxis, ['e', 'g', 'f', 'a', 'b', 'd']) + failing_test_cases = [("mixed", ['A', 3.14]), ("number integer", ['1', 1]), ("string integer", ['42', 42]), diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 7dff100978b9..a8faa9be3782 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,8 +1,9 @@ from __future__ import annotations -import sys import itertools +import pathlib import pickle +import sys from typing import Any from unittest.mock import patch, Mock @@ -180,6 +181,15 @@ def test_boxplot_stats_autorange_false(self): assert_array_almost_equal(bstats_true[0]['fliers'], []) +class Hashable: + def dummy(self): pass + + +class Unhashable: + __hash__ = None # type: ignore[assignment] + def dummy(self): pass + + class Test_callback_registry: def setup_method(self): self.signal = 'test' @@ -195,20 +205,20 @@ def disconnect(self, cid): return self.callbacks.disconnect(cid) def count(self): - count1 = len(self.callbacks._func_cid_map.get(self.signal, [])) + count1 = sum(s == self.signal for s, p in self.callbacks._func_cid_map) count2 = len(self.callbacks.callbacks.get(self.signal)) assert count1 == count2 return count1 def is_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map == {} + assert [*self.callbacks._func_cid_map] == [] assert self.callbacks.callbacks == {} assert self.callbacks._pickled_cids == set() def is_not_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map != {} + assert [*self.callbacks._func_cid_map] != [] assert self.callbacks.callbacks != {} def test_cid_restore(self): @@ -219,12 +229,13 @@ def test_cid_restore(self): assert cid == 1 @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_complete(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_complete(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -235,7 +246,7 @@ def test_callback_complete(self, pickle): cid2 = self.connect(self.signal, mini_me.dummy, pickle) assert cid1 == cid2 self.is_not_empty() - assert len(self.callbacks._func_cid_map) == 1 + assert len([*self.callbacks._func_cid_map]) == 1 assert len(self.callbacks.callbacks) == 1 del mini_me @@ -244,12 +255,13 @@ def test_callback_complete(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -262,12 +274,13 @@ def test_callback_disconnect(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_wrong_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_wrong_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -280,20 +293,21 @@ def test_callback_wrong_disconnect(self, pickle): self.is_not_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_registration_on_non_empty_registry(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_registration_on_non_empty_registry(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # setup the registry with a callback - mini_me = Test_callback_registry() + mini_me = cls() self.connect(self.signal, mini_me.dummy, pickle) # Add another callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # Remove and add the second callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # We still have 2 references @@ -305,9 +319,6 @@ def test_registration_on_non_empty_registry(self, pickle): mini_me2 = None self.is_empty() - def dummy(self): - pass - def test_pickling(self): assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())), "callbacks") @@ -466,8 +477,7 @@ def test_sanitize_sequence(): @pytest.mark.parametrize('inp, kwargs_to_norm', fail_mapping) def test_normalize_kwargs_fail(inp, kwargs_to_norm): - with pytest.raises(TypeError), \ - _api.suppress_matplotlib_deprecation_warning(): + with pytest.raises(TypeError), _api.suppress_matplotlib_deprecation_warning(): cbook.normalize_kwargs(inp, **kwargs_to_norm) @@ -479,6 +489,22 @@ def test_normalize_kwargs_pass(inp, expected, kwargs_to_norm): assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm) +def test_warn_external(recwarn): + _api.warn_external("oops") + assert len(recwarn) == 1 + if sys.version_info[:2] >= (3, 12): + # With Python 3.12, we let Python figure out the stacklevel using the + # `skip_file_prefixes` argument, which cannot exempt tests, so just confirm + # the filename is not in the package. + basedir = pathlib.Path(__file__).parents[2] + assert not recwarn[0].filename.startswith((str(basedir / 'matplotlib'), + str(basedir / 'mpl_toolkits'))) + else: + # On older Python versions, we manually calculated the stacklevel, and had an + # exception for our own tests. + assert recwarn[0].filename == __file__ + + def test_warn_external_frame_embedded_python(): with patch.object(cbook, "sys") as mock_sys: mock_sys._getframe = Mock(return_value=None) @@ -785,12 +811,6 @@ def test_safe_first_element_pandas_series(pd): assert actual == 0 -def test_warn_external(recwarn): - _api.warn_external("oops") - assert len(recwarn) == 1 - assert recwarn[0].filename == __file__ - - def test_array_patch_perimeters(): # This compares the old implementation as a reference for the # vectorized one. @@ -799,8 +819,8 @@ def check(x, rstride, cstride): row_inds = [*range(0, rows-1, rstride), rows-1] col_inds = [*range(0, cols-1, cstride), cols-1] polys = [] - for rs, rs_next in zip(row_inds[:-1], row_inds[1:]): - for cs, cs_next in zip(col_inds[:-1], col_inds[1:]): + for rs, rs_next in itertools.pairwise(row_inds): + for cs, cs_next in itertools.pairwise(col_inds): # +1 ensures we share edges between polygons ps = cbook._array_perimeter(x[rs:rs_next+1, cs:cs_next+1]).T polys.append(ps) @@ -963,7 +983,10 @@ def __array__(self): torch_tensor = torch.Tensor(data) result = cbook._unpack_to_numpy(torch_tensor) - assert result is torch_tensor.__array__() + # compare results, do not check for identity: the latter would fail + # if not mocked, and the implementation does not guarantee it + # is the same Python object, just the same values. + assert_array_equal(result, data) def test_unpack_to_numpy_from_jax(): @@ -988,4 +1011,36 @@ def __array__(self): jax_array = jax.Array(data) result = cbook._unpack_to_numpy(jax_array) - assert result is jax_array.__array__() + # compare results, do not check for identity: the latter would fail + # if not mocked, and the implementation does not guarantee it + # is the same Python object, just the same values. + assert_array_equal(result, data) + + +def test_unpack_to_numpy_from_tensorflow(): + """ + Test that tensorflow arrays are converted to NumPy arrays. + + We don't want to create a dependency on tensorflow in the test suite, so we mock it. + """ + class Tensor: + def __init__(self, data): + self.data = data + + def __array__(self): + return self.data + + tensorflow = ModuleType('tensorflow') + tensorflow.is_tensor = lambda x: isinstance(x, Tensor) + tensorflow.Tensor = Tensor + + sys.modules['tensorflow'] = tensorflow + + data = np.arange(10) + tf_tensor = tensorflow.Tensor(data) + + result = cbook._unpack_to_numpy(tf_tensor) + # compare results, do not check for identity: the latter would fail + # if not mocked, and the implementation does not guarantee it + # is the same Python object, just the same values. + assert_array_equal(result, data) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index c4f98d4eeb45..11934cfca2c3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -17,6 +17,7 @@ import matplotlib.transforms as mtransforms from matplotlib.collections import (Collection, LineCollection, EventCollection, PolyCollection) +from matplotlib.collections import FillBetweenPolyCollection from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -455,7 +456,7 @@ def test_EllipseCollection_setter_getter(): @image_comparison(['polycollection_close.png'], remove_text=True, style='mpl20') def test_polycollection_close(): - from mpl_toolkits.mplot3d import Axes3D # type: ignore + from mpl_toolkits.mplot3d import Axes3D # type: ignore[import] plt.rcParams['axes3d.automargin'] = True vertsQuad = [ @@ -518,7 +519,7 @@ def get_transform(self): """Return transform scaling circle areas to data space.""" ax = self.axes - pts2pixels = 72.0 / ax.figure.dpi + pts2pixels = 72.0 / ax.get_figure(root=True).dpi scale_x = pts2pixels * ax.bbox.width / ax.viewLim.width scale_y = pts2pixels * ax.bbox.height / ax.viewLim.height @@ -830,6 +831,35 @@ def test_collection_set_verts_array(): assert np.array_equal(ap._codes, atp._codes) +@check_figures_equal(extensions=["png"]) +@pytest.mark.parametrize("kwargs", [{}, {"step": "pre"}]) +def test_fill_between_poly_collection_set_data(fig_test, fig_ref, kwargs): + t = np.linspace(0, 16) + f1 = np.sin(t) + f2 = f1 + 0.2 + + fig_ref.subplots().fill_between(t, f1, f2, **kwargs) + + coll = fig_test.subplots().fill_between(t, -1, 1.2, **kwargs) + coll.set_data(t, f1, f2) + + +@pytest.mark.parametrize(("t_direction", "f1", "shape", "where", "msg"), [ + ("z", None, None, None, r"t_direction must be 'x' or 'y', got 'z'"), + ("x", None, (-1, 1), None, r"'x' is not 1-dimensional"), + ("x", None, None, [False] * 3, r"where size \(3\) does not match 'x' size \(\d+\)"), + ("y", [1, 2], None, None, r"'y' has size \d+, but 'x1' has an unequal size of \d+"), +]) +def test_fill_between_poly_collection_raise(t_direction, f1, shape, where, msg): + t = np.linspace(0, 16) + f1 = np.sin(t) if f1 is None else np.asarray(f1) + f2 = f1 + 0.2 + if shape: + t = t.reshape(*shape) + with pytest.raises(ValueError, match=msg): + FillBetweenPolyCollection(t_direction, t, f1, f2, where=where) + + def test_collection_set_array(): vals = [*range(10)] @@ -965,11 +995,6 @@ def test_polyquadmesh_masked_vertices_array(): # Poly version should have the same facecolors as the end of the quadmesh assert_array_equal(quadmesh_fc, polymesh.get_facecolor()) - # Setting array with 1D compressed values is deprecated - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Setting a PolyQuadMesh"): - polymesh.set_array(np.ones(5)) - # We should also be able to call set_array with a new mask and get # updated polys # Remove mask, should add all polys back @@ -1336,3 +1361,26 @@ def test_striped_lines(fig_test, fig_ref, gapcolor): for x, gcol, ls in zip(x, itertools.cycle(gapcolor), itertools.cycle(linestyles)): ax_ref.axvline(x, 0, 1, linewidth=20, linestyle=ls, gapcolor=gcol, alpha=0.5) + + +@check_figures_equal(extensions=['png', 'pdf', 'svg', 'eps']) +def test_hatch_linewidth(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + lw = 2.0 + + polygons = [ + [(0.1, 0.1), (0.1, 0.4), (0.4, 0.4), (0.4, 0.1)], + [(0.6, 0.6), (0.6, 0.9), (0.9, 0.9), (0.9, 0.6)], + ] + ref = PolyCollection(polygons, hatch="x") + ref.set_hatch_linewidth(lw) + + with mpl.rc_context({"hatch.linewidth": lw}): + test = PolyCollection(polygons, hatch="x") + + ax_ref.add_collection(ref) + ax_test.add_collection(test) + + assert test.get_hatch_linewidth() == ref.get_hatch_linewidth() == lw diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 35911afc7952..24eeab689424 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1139,6 +1139,7 @@ def test_colorbar_set_formatter_locator(): fmt = LogFormatter() cb.minorformatter = fmt assert cb.ax.yaxis.get_minor_formatter() is fmt + assert cb.long_axis is cb.ax.yaxis @image_comparison(['colorbar_extend_alpha.png'], remove_text=True, @@ -1180,12 +1181,12 @@ def test_title_text_loc(): def test_passing_location(fig_ref, fig_test): ax_ref = fig_ref.add_subplot() im = ax_ref.imshow([[0, 1], [2, 3]]) - ax_ref.figure.colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]), - orientation="horizontal", ticklocation="top") + ax_ref.get_figure().colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]), + orientation="horizontal", ticklocation="top") ax_test = fig_test.add_subplot() im = ax_test.imshow([[0, 1], [2, 3]]) - ax_test.figure.colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]), - location="top") + ax_test.get_figure().colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]), + location="top") @pytest.mark.parametrize("kwargs,error,message", [ diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 4fd9f86c06e3..cc6cb1bb11a7 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -15,6 +15,7 @@ import matplotlib as mpl import matplotlib.colors as mcolors import matplotlib.colorbar as mcolorbar +import matplotlib.colorizer as mcolorizer import matplotlib.pyplot as plt import matplotlib.scale as mscale from matplotlib.rcsetup import cycler @@ -1415,7 +1416,7 @@ def test_ndarray_subclass_norm(): # which objects when adding or subtracting with other # arrays. See #6622 and #8696 class MyArray(np.ndarray): - def __isub__(self, other): # type: ignore + def __isub__(self, other): # type: ignore[misc] raise RuntimeError def __add__(self, other): @@ -1634,7 +1635,7 @@ 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'] + 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff10'] assert len(plt.color_sequences['tab10']) == 10 assert len(plt.color_sequences['tab20']) == 20 @@ -1689,6 +1690,11 @@ def test_set_cmap_mismatched_name(): assert cmap_returned.name == "wrong-cmap" +def test_cmap_alias_names(): + assert matplotlib.colormaps["gray"].name == "gray" # original + assert matplotlib.colormaps["grey"].name == "grey" # alias + + def test_to_rgba_array_none_color_with_alpha_param(): # effective alpha for color "none" must always be 0 to achieve a vanishing color # even explicit alpha must be ignored @@ -1710,3 +1716,15 @@ def test_to_rgba_array_none_color_with_alpha_param(): (('C3', 0.5), True)]) def test_is_color_like(input, expected): assert is_color_like(input) is expected + + +def test_colorizer_vmin_vmax(): + ca = mcolorizer.Colorizer() + assert ca.vmin is None + assert ca.vmax is None + ca.vmin = 1 + ca.vmax = 3 + assert ca.vmin == 1.0 + assert ca.vmax == 3.0 + assert ca.norm.vmin == 1.0 + assert ca.norm.vmax == 3.0 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 4dc4d9501ec1..e42e2ee9bfd8 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -662,6 +662,30 @@ def test_compressed1(): np.testing.assert_allclose(pos.y0, 0.1934, atol=1e-3) +def test_compressed_suptitle(): + fig, (ax0, ax1) = plt.subplots( + nrows=2, figsize=(4, 10), layout="compressed", + gridspec_kw={"height_ratios": (1 / 4, 3 / 4), "hspace": 0}) + + ax0.axis("equal") + ax0.set_box_aspect(1/3) + + ax1.axis("equal") + ax1.set_box_aspect(1) + + title = fig.suptitle("Title") + fig.draw_without_rendering() + assert title.get_position()[1] == pytest.approx(0.7457, abs=1e-3) + + title = fig.suptitle("Title", y=0.98) + fig.draw_without_rendering() + assert title.get_position()[1] == 0.98 + + title = fig.suptitle("Title", in_layout=False) + fig.draw_without_rendering() + assert title.get_position()[1] == 0.98 + + @pytest.mark.parametrize('arg, state', [ (True, True), (False, False), diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index a3e00c30ce97..e0ea82973af7 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -5,8 +5,7 @@ import contourpy import numpy as np -from numpy.testing import ( - assert_array_almost_equal, assert_array_almost_equal_nulp, assert_array_equal) +from numpy.testing import assert_array_almost_equal, assert_array_almost_equal_nulp import matplotlib as mpl from matplotlib import pyplot as plt, rc_context, ticker from matplotlib.colors import LogNorm, same_color @@ -15,19 +14,6 @@ import pytest -# Helper to test the transition from ContourSets holding multiple Collections to being a -# single Collection; remove once the deprecated old layout expires. -def _maybe_split_collections(do_split): - if not do_split: - return - for fig in map(plt.figure, plt.get_fignums()): - for ax in fig.axes: - for coll in ax.collections: - if isinstance(coll, mpl.contour.ContourSet): - with pytest.warns(mpl._api.MatplotlibDeprecationWarning): - coll.collections - - def test_contour_shape_1d_valid(): x = np.arange(10) @@ -108,17 +94,14 @@ def test_contour_set_paths(fig_test, fig_ref): cs_test.set_paths(cs_ref.get_paths()) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20', tol=0.26) -def test_contour_manual_labels(split_collections): +def test_contour_manual_labels(): x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10)) z = np.max(np.dstack([abs(x), abs(y)]), 2) plt.figure(figsize=(6, 2), dpi=200) cs = plt.contour(x, y, z) - _maybe_split_collections(split_collections) - pts = np.array([(1.0, 3.0), (1.0, 4.4), (1.0, 6.0)]) plt.clabel(cs, manual=pts) pts = np.array([(2.0, 3.0), (2.0, 4.4), (2.0, 6.0)]) @@ -144,29 +127,21 @@ def test_contour_manual_moveto(): assert clabels[0].get_text() == "0" -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_disconnected_segments'], remove_text=True, style='mpl20', extensions=['png']) -def test_contour_label_with_disconnected_segments(split_collections): +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) z += 1 / np.sqrt(0.01 + (x - 0.3) ** 2 + y ** 2) plt.figure() cs = plt.contour(x, y, z, levels=[7]) - - # Adding labels should invalidate the old style - _maybe_split_collections(split_collections) - cs.clabel(manual=[(0.2, 0.1)]) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_manual_colors_and_levels.png'], remove_text=True, tol=0.018 if platform.machine() == 'arm64' else 0) -def test_given_colors_levels_and_extends(split_collections): +def test_given_colors_levels_and_extends(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -195,8 +170,6 @@ def test_given_colors_levels_and_extends(split_collections): plt.colorbar(c, ax=ax) - _maybe_split_collections(split_collections) - @image_comparison(['contourf_hatch_colors'], remove_text=True, style='mpl20', extensions=['png']) @@ -206,9 +179,22 @@ def test_hatch_colors(): cf.set_edgecolors(["blue", "grey", "yellow", "red"]) -@pytest.mark.parametrize("split_collections", [False, True]) +@pytest.mark.parametrize('color, extend', [('darkred', 'neither'), + ('darkred', 'both'), + (('r', 0.5), 'neither'), + ((0.1, 0.2, 0.5, 0.3), 'neither')]) +def test_single_color_and_extend(color, extend): + z = [[0, 1], [1, 2]] + + _, ax = plt.subplots() + levels = [0.5, 0.75, 1, 1.25, 1.5] + cs = ax.contour(z, levels=levels, colors=color, extend=extend) + for c in cs.get_edgecolors(): + assert same_color(c, color) + + @image_comparison(['contour_log_locator.svg'], style='mpl20', remove_text=False) -def test_log_locator_levels(split_collections): +def test_log_locator_levels(): fig, ax = plt.subplots() @@ -227,12 +213,9 @@ def test_log_locator_levels(split_collections): cb = fig.colorbar(c, ax=ax) assert_array_almost_equal(cb.ax.get_yticks(), c.levels) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_datetime_axis.png'], style='mpl20') -def test_contour_datetime_axis(split_collections): +def test_contour_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) base = datetime.datetime(2013, 1, 1) @@ -255,13 +238,10 @@ def test_contour_datetime_axis(split_collections): label.set_ha('right') label.set_rotation(30) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_test_label_transforms.png'], remove_text=True, style='mpl20', tol=1.1) -def test_labels(split_collections): +def test_labels(): # Adapted from pylab_examples example code: contour_demo.py # see issues #2475, #2843, and #2818 for explanation delta = 0.025 @@ -280,9 +260,6 @@ def test_labels(split_collections): disp_units = [(216, 177), (359, 290), (521, 406)] data_units = [(-2, .5), (0, -1.5), (2.8, 1)] - # Adding labels should invalidate the old style - _maybe_split_collections(split_collections) - CS.clabel() for x, y in data_units: @@ -291,8 +268,6 @@ def test_labels(split_collections): for x, y in disp_units: CS.add_label_near(x, y, inline=True, transform=False) - _maybe_split_collections(split_collections) - def test_label_contour_start(): # Set up data and figure/axes that result in automatic labelling adding the @@ -319,10 +294,9 @@ def test_label_contour_start(): assert 0 in idxs -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_corner_mask_False.png', 'contour_corner_mask_True.png'], remove_text=True, tol=1.88) -def test_corner_mask(split_collections): +def test_corner_mask(): n = 60 mask_level = 0.95 noise_amp = 1.0 @@ -336,8 +310,6 @@ def test_corner_mask(split_collections): plt.figure() plt.contourf(z, corner_mask=corner_mask) - _maybe_split_collections(split_collections) - def test_contourf_decreasing_levels(): # github issue 5477. @@ -408,11 +380,10 @@ def test_clabel_with_large_spacing(): # tol because ticks happen to fall on pixel boundaries so small # floating point changes in tick location flip which pixel gets # the tick. -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_log_extension.png'], remove_text=True, style='mpl20', tol=1.444) -def test_contourf_log_extension(split_collections): +def test_contourf_log_extension(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -444,17 +415,14 @@ def test_contourf_log_extension(split_collections): assert_array_almost_equal_nulp(cb.ax.get_ylim(), np.array((1e-4, 1e6))) cb = plt.colorbar(c3, ax=ax3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison( ['contour_addlines.png'], remove_text=True, style='mpl20', tol=0.15 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0.03) # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... -def test_contour_addlines(split_collections): +def test_contour_addlines(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -468,13 +436,10 @@ def test_contour_addlines(split_collections): cb.add_lines(cont) assert_array_almost_equal(cb.ax.get_ylim(), [114.3091, 9972.30735], 3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_uneven'], extensions=['png'], remove_text=True, style='mpl20') -def test_contour_uneven(split_collections): +def test_contour_uneven(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -487,8 +452,6 @@ def test_contour_uneven(split_collections): cs = ax.contourf(z, levels=[2, 4, 6, 10, 20]) fig.colorbar(cs, ax=ax, spacing='uniform') - _maybe_split_collections(split_collections) - @pytest.mark.parametrize( "rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected", [ @@ -505,8 +468,6 @@ def test_contour_linewidth( X = np.arange(4*3).reshape(4, 3) cs = ax.contour(X, linewidths=call_linewidths) assert cs.get_linewidths()[0] == expected - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"): - assert cs.tlinewidths[0][0] == expected @pytest.mark.backend("pdf") @@ -515,10 +476,9 @@ def test_label_nonagg(): plt.clabel(plt.contour([[1, 2], [3, 4]])) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_closed_line_loop'], extensions=['png'], remove_text=True) -def test_contour_closed_line_loop(split_collections): +def test_contour_closed_line_loop(): # github issue 19568. z = [[0, 0, 0], [0, 2, 0], [0, 0, 0], [2, 1, 2]] @@ -527,8 +487,6 @@ def test_contour_closed_line_loop(split_collections): ax.set_xlim(-0.1, 2.1) ax.set_ylim(-0.1, 3.1) - _maybe_split_collections(split_collections) - def test_quadcontourset_reuse(): # If QuadContourSet returned from one contour(f) call is passed as first @@ -543,10 +501,9 @@ def test_quadcontourset_reuse(): assert qcs3._contour_generator == qcs1._contour_generator -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_manual'], extensions=['png'], remove_text=True, tol=0.89) -def test_contour_manual(split_collections): +def test_contour_manual(): # Manually specifying contour lines/polygons to plot. from matplotlib.contour import ContourSet @@ -569,13 +526,10 @@ def test_contour_manual(split_collections): ContourSet(ax, [2, 3], [segs], [kinds], filled=True, cmap=cmap) ContourSet(ax, [2], [segs], [kinds], colors='k', linewidths=3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_line_start_on_corner_edge'], extensions=['png'], remove_text=True) -def test_contour_line_start_on_corner_edge(split_collections): +def test_contour_line_start_on_corner_edge(): fig, ax = plt.subplots(figsize=(6, 5)) x, y = np.meshgrid([0, 1, 2, 3, 4], [0, 1, 2]) @@ -589,8 +543,6 @@ def test_contour_line_start_on_corner_edge(split_collections): lines = ax.contour(x, y, z, corner_mask=True, colors='k') cbar.add_lines(lines) - _maybe_split_collections(split_collections) - def test_find_nearest_contour(): xy = np.indices((15, 15)) @@ -711,10 +663,9 @@ def test_algorithm_supports_corner_mask(algorithm): plt.contourf(z, algorithm=algorithm, corner_mask=True) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_all_algorithms'], extensions=['png'], remove_text=True, tol=0.06) -def test_all_algorithms(split_collections): +def test_all_algorithms(): algorithms = ['mpl2005', 'mpl2014', 'serial', 'threaded'] rng = np.random.default_rng(2981) @@ -730,8 +681,6 @@ def test_all_algorithms(split_collections): ax.contour(x, y, z, algorithm=algorithm, colors='k') ax.set_title(algorithm) - _maybe_split_collections(split_collections) - def test_subfigure_clabel(): # Smoke test for gh#23173 @@ -890,19 +839,3 @@ def test_allsegs_allkinds(): assert len(result) == 2 assert len(result[0]) == 5 assert len(result[1]) == 4 - - -def test_deprecated_apis(): - cs = plt.contour(np.arange(16).reshape((4, 4))) - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="collections"): - colls = cs.collections - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tcolors"): - assert_array_equal(cs.tcolors, [c.get_edgecolor() for c in colls]) - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"): - assert cs.tlinewidths == [c.get_linewidth() for c in colls] - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - assert cs.antialiased - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - cs.antialiased = False - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - assert not cs.antialiased diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 4133524e0e1a..73f10cec52aa 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -636,6 +636,23 @@ def test_concise_formatter_show_offset(t_delta, expected): assert formatter.get_offset() == expected +def test_concise_formatter_show_offset_inverted(): + # Test for github issue #28481 + d1 = datetime.datetime(1997, 1, 1) + d2 = d1 + datetime.timedelta(days=60) + + fig, ax = plt.subplots() + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + ax.invert_xaxis() + + ax.plot([d1, d2], [0, 0]) + fig.canvas.draw() + assert formatter.get_offset() == '1997-Jan' + + def test_concise_converter_stays(): # This test demonstrates problems introduced by gh-23417 (reverted in gh-25278) # In particular, downstream libraries like Pandas had their designated converters @@ -651,10 +668,12 @@ def test_concise_converter_stays(): fig, ax = plt.subplots() ax.plot(x, y) # Bypass Switchable date converter - ax.xaxis.converter = conv = mdates.ConciseDateConverter() + conv = mdates.ConciseDateConverter() + with pytest.warns(UserWarning, match="already has a converter"): + ax.xaxis.set_converter(conv) assert ax.xaxis.units is None ax.set_xlim(*x) - assert ax.xaxis.converter == conv + assert ax.xaxis.get_converter() == conv def test_offset_changes(): diff --git a/lib/matplotlib/tests/test_datetime.py b/lib/matplotlib/tests/test_datetime.py index 4b693eb7d1ca..276056d044ae 100644 --- a/lib/matplotlib/tests/test_datetime.py +++ b/lib/matplotlib/tests/test_datetime.py @@ -255,7 +255,7 @@ def test_bxp(self): datetime.datetime(2020, 1, 27) ] }] - ax.bxp(data, vert=False) + ax.bxp(data, orientation='horizontal') ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%Y-%m-%d")) ax.set_title('Box plot with datetime data') diff --git a/lib/matplotlib/tests/test_determinism.py b/lib/matplotlib/tests/test_determinism.py index 3865dbc7fa43..2ecc40dbd3c0 100644 --- a/lib/matplotlib/tests/test_determinism.py +++ b/lib/matplotlib/tests/test_determinism.py @@ -8,13 +8,21 @@ import pytest import matplotlib as mpl -import matplotlib.testing.compare from matplotlib import pyplot as plt -from matplotlib.testing._markers import needs_ghostscript, needs_usetex +from matplotlib.cbook import get_sample_data +from matplotlib.collections import PathCollection +from matplotlib.image import BboxImage +from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox +from matplotlib.patches import Circle, PathPatch +from matplotlib.path import Path from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing._markers import needs_ghostscript, needs_usetex +import matplotlib.testing.compare +from matplotlib.text import TextPath +from matplotlib.transforms import IdentityTransform -def _save_figure(objects='mhi', fmt="pdf", usetex=False): +def _save_figure(objects='mhip', fmt="pdf", usetex=False): mpl.use(fmt) mpl.rcParams.update({'svg.hashsalt': 'asdf', 'text.usetex': usetex}) @@ -50,6 +58,76 @@ def _save_figure(objects='mhi', fmt="pdf", usetex=False): A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]] fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic') + if 'p' in objects: + + # clipping support class, copied from demo_text_path.py gallery example + class PathClippedImagePatch(PathPatch): + """ + The given image is used to draw the face of the patch. Internally, + it uses BboxImage whose clippath set to the path of the patch. + + FIXME : The result is currently dpi dependent. + """ + + def __init__(self, path, bbox_image, **kwargs): + super().__init__(path, **kwargs) + self.bbox_image = BboxImage( + self.get_window_extent, norm=None, origin=None) + self.bbox_image.set_data(bbox_image) + + def set_facecolor(self, color): + """Simply ignore facecolor.""" + super().set_facecolor("none") + + def draw(self, renderer=None): + # the clip path must be updated every draw. any solution? -JJ + self.bbox_image.set_clip_path(self._path, self.get_transform()) + self.bbox_image.draw(renderer) + super().draw(renderer) + + # add a polar projection + px = fig.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) + arr = plt.imread(get_sample_data("grace_hopper.jpg")) + text_path = TextPath((0, 0), "!?", size=150) + p = PathClippedImagePatch(text_path, arr, ec="k") + offsetbox = AuxTransformBox(IdentityTransform()) + offsetbox.add_artist(p) + ao = AnchoredOffsetbox(loc='upper left', child=offsetbox, frameon=True, + borderpad=0.2) + ax1.add_artist(ao) + + # add a 2x2 grid of path-clipped axes (origin: test_artist.py) + exterior = Path.unit_rectangle().deepcopy() + exterior.vertices *= 4 + exterior.vertices -= 2 + interior = Path.unit_circle().deepcopy() + interior.vertices = interior.vertices[::-1] + clip_path = Path.make_compound_path(exterior, interior) + + star = Path.unit_regular_star(6).deepcopy() + star.vertices *= 2.6 + + (row1, row2) = fig.subplots(2, 2, sharex=True, sharey=True) + for row in (row1, row2): + ax1, ax2 = row + collection = PathCollection([star], lw=5, edgecolor='blue', + facecolor='red', alpha=0.7, hatch='*') + collection.set_clip_path(clip_path, ax1.transData) + ax1.add_collection(collection) + + patch = PathPatch(star, lw=5, edgecolor='blue', facecolor='red', + alpha=0.7, hatch='*') + patch.set_clip_path(clip_path, ax2.transData) + ax2.add_patch(patch) + + ax1.set_xlim([-3, 3]) + ax1.set_ylim([-3, 3]) + x = range(5) ax = fig.add_subplot(1, 6, 6) ax.plot(x, x) @@ -67,12 +145,13 @@ def _save_figure(objects='mhi', fmt="pdf", usetex=False): ("m", "pdf", False), ("h", "pdf", False), ("i", "pdf", False), - ("mhi", "pdf", False), - ("mhi", "ps", False), + ("mhip", "pdf", False), + ("mhip", "ps", False), pytest.param( - "mhi", "ps", True, marks=[needs_usetex, needs_ghostscript]), - ("mhi", "svg", False), - pytest.param("mhi", "svg", True, marks=needs_usetex), + "mhip", "ps", True, marks=[needs_usetex, needs_ghostscript]), + ("p", "svg", False), + ("mhip", "svg", False), + pytest.param("mhip", "svg", True, marks=needs_usetex), ] ) def test_determinism_check(objects, fmt, usetex): @@ -84,7 +163,7 @@ def test_determinism_check(objects, fmt, usetex): ---------- objects : str Objects to be included in the test document: 'm' for markers, 'h' for - hatch patterns, 'i' for images. + hatch patterns, 'i' for images, and 'p' for paths. fmt : {"pdf", "ps", "svg"} Output format. """ diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 6e6daa77062d..edf5ea05f119 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -151,6 +151,29 @@ def test_figure_label(): plt.figure(Figure()) +def test_figure_label_replaced(): + plt.close('all') + fig = plt.figure(1) + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match="Changing 'Figure.number' is deprecated"): + fig.number = 2 + assert fig.number == 2 + + +def test_figure_no_label(): + # standalone figures do not have a figure attribute + fig = Figure() + with pytest.raises(AttributeError): + fig.number + # but one can set one + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match="Changing 'Figure.number' is deprecated"): + fig.number = 5 + assert fig.number == 5 + # even though it's not known by pyplot + assert not plt.fignum_exists(fig.number) + + def test_fignum_exists(): # pyplot figure creation, selection and closing with fignum_exists plt.figure('one') @@ -473,6 +496,20 @@ def test_autofmt_xdate(which): assert int(label.get_rotation()) == angle +def test_autofmt_xdate_colorbar_constrained(): + # check works with a colorbar. + # with constrained layout, colorbars do not have a gridspec, + # but autofmt_xdate checks if all axes have a gridspec before being + # applied. + fig, ax = plt.subplots(layout="constrained") + im = ax.imshow([[1, 4, 6], [2, 3, 5]]) + plt.colorbar(im) + fig.autofmt_xdate() + fig.draw_without_rendering() + label = ax.get_xticklabels(which='major')[1] + assert label.get_rotation() == 30.0 + + @mpl.style.context('default') def test_change_dpi(): fig = plt.figure(figsize=(4, 4)) @@ -518,12 +555,10 @@ def test_invalid_figure_add_axes(): fig.add_axes(ax) fig2.delaxes(ax) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig2.add_axes(ax, "extra positional argument") - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig.add_axes([0, 0, 1, 1], "extra positional argument") @@ -1735,6 +1770,30 @@ def test_warn_colorbar_mismatch(): subfig3_1.colorbar(im4_1) +def test_set_figure(): + fig = plt.figure() + sfig1 = fig.subfigures() + sfig2 = sfig1.subfigures() + + for f in fig, sfig1, sfig2: + with pytest.warns(mpl.MatplotlibDeprecationWarning): + f.set_figure(fig) + + with pytest.raises(ValueError, match="cannot be changed"): + sfig2.set_figure(sfig1) + + with pytest.raises(ValueError, match="cannot be changed"): + sfig1.set_figure(plt.figure()) + + +def test_subfigure_row_order(): + # Test that subfigures are drawn in row-major order. + fig = plt.figure() + sf_arr = fig.subfigures(4, 3) + for a, b in zip(sf_arr.ravel(), fig.subfigs): + assert a is b + + def test_subfigure_stale_propagation(): fig = plt.figure() @@ -1750,10 +1809,13 @@ def test_subfigure_stale_propagation(): sfig2 = sfig1.subfigures() assert fig.stale + assert sfig1.stale fig.draw_without_rendering() assert not fig.stale + assert not sfig1.stale assert not sfig2.stale sfig2.stale = True + assert sfig1.stale assert fig.stale diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 776af16eeaaf..d15b892b3eea 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -11,6 +11,7 @@ import numpy as np import pytest +import matplotlib as mpl from matplotlib.font_manager import ( findfont, findSystemFonts, FontEntry, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, @@ -183,8 +184,8 @@ def test_addfont_as_path(): path = Path(__file__).parent / font_test_file try: fontManager.addfont(path) - added, = [font for font in fontManager.ttflist - if font.fname.endswith(font_test_file)] + added, = (font for font in fontManager.ttflist + if font.fname.endswith(font_test_file)) fontManager.ttflist.remove(added) finally: to_remove = [font for font in fontManager.ttflist @@ -250,17 +251,22 @@ def test_missing_family(caplog): def _test_threading(): import threading - from matplotlib.ft2font import LOAD_NO_HINTING + from matplotlib.ft2font import LoadFlags import matplotlib.font_manager as fm + def loud_excepthook(args): + raise RuntimeError("error in thread!") + + threading.excepthook = loud_excepthook + N = 10 b = threading.Barrier(N) def bad_idea(n): - b.wait() + b.wait(timeout=5) for j in range(100): font = fm.get_font(fm.findfont("DejaVu Sans")) - font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING) + font.set_text(str(n), 0.0, flags=LoadFlags.NO_HINTING) threads = [ threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,)) @@ -271,7 +277,9 @@ def bad_idea(n): t.start() for t in threads: - t.join() + t.join(timeout=9) + if t.is_alive(): + raise RuntimeError("thread failed to join") def test_fontcache_thread_safe(): @@ -360,3 +368,42 @@ def inner(): for obj in gc.get_objects(): if isinstance(obj, SomeObject): pytest.fail("object from inner stack still alive") + + +def test_fontproperties_init_deprecation(): + """ + Test the deprecated API of FontProperties.__init__. + + The deprecation does not change behavior, it only adds a deprecation warning + via a decorator. Therefore, the purpose of this test is limited to check + which calls do and do not issue deprecation warnings. Behavior is still + tested via the existing regular tests. + """ + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # multiple positional arguments + FontProperties("Times", "italic") + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # Mixed positional and keyword arguments + FontProperties("Times", size=10) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # passing a family list positionally + FontProperties(["Times"]) + + # still accepted: + FontProperties(family="Times", style="italic") + FontProperties(family="Times") + FontProperties("Times") # works as pattern and family + FontProperties("serif-24:style=oblique:weight=bold") # pattern + + # also still accepted: + # passing as pattern via family kwarg was not covered by the docs but + # historically worked. This is left unchanged for now. + # AFAICT, we cannot detect this: We can determine whether a string + # works as pattern, but that doesn't help, because there are strings + # that are both pattern and family. We would need to identify, whether + # a string is *not* a valid family. + # Since this case is not covered by docs, I've refrained from jumping + # extra hoops to detect this possible API misuse. + FontProperties(family="serif-24:style=oblique:weight=bold") diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 2e2ce673f4b8..7dc851b2c9cf 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,33 +1,855 @@ -from pathlib import Path +import itertools import io +from pathlib import Path +import numpy as np import pytest +import matplotlib as mpl from matplotlib import ft2font from matplotlib.testing.decorators import check_figures_equal import matplotlib.font_manager as fm +import matplotlib.path as mpath import matplotlib.pyplot as plt -def test_fallback_errors(): - file_name = fm.findfont('DejaVu Sans') +def test_ft2image_draw_rect_filled(): + width = 23 + height = 42 + for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]): + im = ft2font.FT2Image(width, height) + im.draw_rect_filled(x0, y0, x1, y1) + a = np.asarray(im) + assert a.dtype == np.uint8 + assert a.shape == (height, width) + if x0 == 100 or y0 == 200: + # All the out-of-bounds starts should get automatically clipped. + assert np.sum(a) == 0 + else: + # Otherwise, ends are clipped to the dimension, but are also _inclusive_. + filled = (min(x1 + 1, width) - x0) * (min(y1 + 1, height) - y0) + assert np.sum(a) == 255 * filled + + +def test_ft2font_dejavu_attrs(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'DejaVuSans' + assert font.family_name == 'DejaVu Sans' + assert font.style_name == 'Book' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 6241 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 5 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.KERNING | + ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.NORMAL + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -175 # Underline position. + assert font.underline_thickness == 90 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1901 # HHead Ascent. + assert font.descender == -483 # HHead Descent. + # Unconfirmed values. + assert font.height == 2384 + assert font.max_advance_width == 3838 + assert font.max_advance_height == 2384 + assert font.bbox == (-2090, -948, 3673, 2524) + + +def test_ft2font_cm_attrs(): + file = fm.findfont('cmtt10') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'Cmtt10' + assert font.family_name == 'cmtt10' + assert font.style_name == 'Regular' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 133 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 2 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.NORMAL + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -143 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1276 # HHead Ascent. + assert font.descender == -489 # HHead Descent. + # Unconfirmed values. + assert font.height == 1765 + assert font.max_advance_width == 1536 + assert font.max_advance_height == 1765 + assert font.bbox == (-12, -477, 1280, 1430) + - with pytest.raises(TypeError, match="Fallback list must be a list"): +def test_ft2font_stix_bold_attrs(): + file = fm.findfont('STIXSizeTwoSym:bold') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'STIXSizeTwoSym-Bold' + assert font.family_name == 'STIXSizeTwoSym' + assert font.style_name == 'Bold' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 20 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 3 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.BOLD + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 1000 # Em Size. + assert font.underline_position == -133 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 2095 # HHead Ascent. + assert font.descender == -404 # HHead Descent. + # Unconfirmed values. + assert font.height == 2499 + assert font.max_advance_width == 1130 + assert font.max_advance_height == 2499 + assert font.bbox == (4, -355, 1185, 2095) + + +def test_ft2font_invalid_args(tmp_path): + # filename argument. + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(None) + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(object()) # Not bytes or string, and has no read() method. + file = tmp_path / 'invalid-font.ttf' + file.write_text('This is not a valid font file.') + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('rt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wb') as fd): + ft2font.FT2Font(fd) + + file = fm.findfont('DejaVu Sans') + + # hinting_factor argument. + with pytest.raises(TypeError, match='incompatible constructor arguments'): + ft2font.FT2Font(file, 1.3) + with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): + ft2font.FT2Font(file, 0) + + with pytest.raises(TypeError, match='incompatible constructor arguments'): # failing to be a list will fail before the 0 - ft2font.FT2Font(file_name, _fallback_list=(0,)) # type: ignore[arg-type] + ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type] + with pytest.raises(TypeError, match='incompatible constructor arguments'): + ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] - with pytest.raises( - TypeError, match="Fallback fonts must be FT2Font objects." - ): - ft2font.FT2Font(file_name, _fallback_list=[0]) # type: ignore[list-item] + # kerning_factor argument. + with pytest.raises(TypeError, match='incompatible constructor arguments'): + ft2font.FT2Font(file, _kerning_factor=1.3) -def test_ft2font_positive_hinting_factor(): - file_name = fm.findfont('DejaVu Sans') - with pytest.raises( - ValueError, match="hinting_factor must be greater than 0" - ): - ft2font.FT2Font(file_name, 0) +def test_ft2font_clear(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + font.set_text('ABabCDcd') + assert font.get_num_glyphs() == 8 + assert font.get_width_height() != (0, 0) + assert font.get_bitmap_offset() != (0, 0) + font.clear() + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + + +def test_ft2font_set_size(): + file = fm.findfont('DejaVu Sans') + # Default is 12pt @ 72 dpi. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1) + font.set_text('ABabCDcd') + orig = font.get_width_height() + font.set_size(24, 72) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + font.set_size(12, 144) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + + +def test_ft2font_charmaps(): + def enc(name): + # We don't expose the encoding enum from FreeType, but can generate it here. + # For DejaVu, there are 5 charmaps, but only 2 have enum entries in FreeType. + e = 0 + for x in name: + e <<= 8 + e += ord(x) + return e + + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.num_charmaps == 5 + + # Unicode. + font.select_charmap(enc('unic')) + unic = font.get_charmap() + font.set_charmap(0) # Unicode platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(1) # Unicode platform, modern subtable. + after = font.get_charmap() + assert unic == after + font.set_charmap(3) # Windows platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(4) # Windows platform, Unicode full repertoire, modern subtable. + after = font.get_charmap() + assert unic == after + + # This is just a random sample from FontForge. + glyph_names = { + 'non-existent-glyph-name': 0, + 'plusminus': 115, + 'Racute': 278, + 'perthousand': 2834, + 'seveneighths': 3057, + 'triagup': 3721, + 'uni01D3': 405, + 'uni0417': 939, + 'uni2A02': 4464, + 'u1D305': 5410, + 'u1F0A1': 5784, + } + for name, index in glyph_names.items(): + assert font.get_name_index(name) == index + if name == 'non-existent-glyph-name': + name = '.notdef' + # This doesn't always apply, but it does for DejaVu Sans. + assert font.get_glyph_name(index) == name + + # Apple Roman. + font.select_charmap(enc('armn')) + armn = font.get_charmap() + font.set_charmap(2) # Macintosh platform, Roman. + after = font.get_charmap() + assert armn == after + assert len(armn) <= 256 # 8-bit encoding. + # The first 128 characters of Apple Roman match ASCII, which also matches Unicode. + for o in range(1, 128): + if o not in armn or o not in unic: + continue + assert unic[o] == armn[o] + # Check a couple things outside the ASCII set that are different in each charset. + examples = [ + # (Unicode, Macintosh) + (0x2020, 0xA0), # Dagger. + (0x00B0, 0xA1), # Degree symbol. + (0x00A3, 0xA3), # Pound sign. + (0x00A7, 0xA4), # Section sign. + (0x00B6, 0xA6), # Pilcrow. + (0x221E, 0xB0), # Infinity symbol. + ] + for u, m in examples: + # Though the encoding is different, the glyph should be the same. + assert unic[u] == armn[m] + + +_expected_sfnt_names = { + 'DejaVu Sans': { + 0: 'Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.\n' + 'Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n' + 'DejaVu changes are in public domain\n', + 1: 'DejaVu Sans', + 2: 'Book', + 3: 'DejaVu Sans', + 4: 'DejaVu Sans', + 5: 'Version 2.35', + 6: 'DejaVuSans', + 8: 'DejaVu fonts team', + 11: 'http://dejavu.sourceforge.net', + 13: 'Fonts are (c) Bitstream (see below). ' + 'DejaVu changes are in public domain. ' + '''Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. ''' ''' + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the ''' ''' +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr.''', + 14: 'http://dejavu.sourceforge.net/wiki/index.php/License', + 16: 'DejaVu Sans', + 17: 'Book', + }, + 'cmtt10': { + 0: 'Copyright (C) 1994, Basil K. Malyshev. All Rights Reserved.' + '012BaKoMa Fonts Collection, Level-B.', + 1: 'cmtt10', + 2: 'Regular', + 3: 'FontMonger:cmtt10', + 4: 'cmtt10', + 5: '1.1/12-Nov-94', + 6: 'Cmtt10', + }, + 'STIXSizeTwoSym:bold': { + 0: 'Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the ' + 'American Chemical Society, the American Institute of Physics, the American ' + 'Mathematical Society, the American Physical Society, Elsevier, Inc., and ' + 'The Institute of Electrical and Electronic Engineers, Inc. Portions ' + 'copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by ' + 'Elsevier, Inc. All rights reserved.', + 1: 'STIXSizeTwoSym', + 2: 'Bold', + 3: 'FontMaster:STIXSizeTwoSym-Bold:1.0.0', + 4: 'STIXSizeTwoSym-Bold', + 5: 'Version 1.0.0', + 6: 'STIXSizeTwoSym-Bold', + 7: 'STIX Fonts(TM) is a trademark of The Institute of Electrical and ' + 'Electronics Engineers, Inc.', + 9: 'MicroPress Inc., with final additions and corrections provided by Coen ' + 'Hoffman, Elsevier (retired)', + 10: 'Arie de Ruiter, who in 1995 was Head of Information Technology ' + 'Development at Elsevier Science, made a proposal to the STI Pub group, an ' + 'informal group of publishers consisting of representatives from the ' + 'American Chemical Society (ACS), American Institute of Physics (AIP), ' + 'American Mathematical Society (AMS), American Physical Society (APS), ' + 'Elsevier, and Institute of Electrical and Electronics Engineers (IEEE). ' + 'De Ruiter encouraged the members to consider development of a series of ' + 'Web fonts, which he proposed should be called the Scientific and ' + 'Technical Information eXchange, or STIX, Fonts. All STI Pub member ' + 'organizations enthusiastically endorsed this proposal, and the STI Pub ' + 'group agreed to embark on what has become a twelve-year project. The goal ' + 'of the project was to identify all alphabetic, symbolic, and other ' + 'special characters used in any facet of scientific publishing and to ' + 'create a set of Unicode-based fonts that would be distributed free to ' + 'every scientist, student, and other interested party worldwide. The fonts ' + 'would be consistent with the emerging Unicode standard, and would permit ' + 'universal representation of every character. With the release of the STIX ' + "fonts, de Ruiter's vision has been realized.", + 11: 'http://www.stixfonts.org', + 12: 'http://www.micropress-inc.com', + 13: 'As a condition for receiving these fonts at no charge, each person ' + 'downloading the fonts must agree to some simple license terms. The ' + 'license is based on the SIL Open Font License ' + '. The ' + 'SIL License is a free and open source license specifically designed for ' + 'fonts and related software. The basic terms are that the recipient will ' + 'not remove the copyright and trademark statements from the fonts and ' + 'that, if the person decides to create a derivative work based on the STIX ' + 'Fonts but incorporating some changes or enhancements, the derivative work ' + '("Modified Version") will carry a different name. The copyright and ' + 'trademark restrictions are part of the agreement between the STI Pub ' + 'companies and the typeface designer. The "renaming" restriction results ' + 'from the desire of the STI Pub companies to assure that the STIX Fonts ' + 'will continue to function in a predictable fashion for all that use them. ' + 'No copy of one or more of the individual Font typefaces that form the ' + 'STIX Fonts(TM) set may be sold by itself, but other than this one ' + 'restriction, licensees are free to sell the fonts either separately or as ' + 'part of a package that combines other software or fonts with this font ' + 'set.', + 14: 'http://www.stixfonts.org/user_license.html', + }, +} + + +@pytest.mark.parametrize('font_name, expected', _expected_sfnt_names.items(), + ids=_expected_sfnt_names.keys()) +def test_ft2font_get_sfnt(font_name, expected): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + sfnt = font.get_sfnt() + for name, value in expected.items(): + # Macintosh, Unicode 1.0, English, name. + assert sfnt.pop((1, 0, 0, name)) == value.encode('ascii') + # Microsoft, Unicode, English United States, name. + assert sfnt.pop((3, 1, 1033, name)) == value.encode('utf-16be') + assert sfnt == {} + + +_expected_sfnt_tables = { + 'DejaVu Sans': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (2, 22937), + 'checkSumAdjustment': -175678572, + 'magicNumber': 0x5F0F3CF5, + 'flags': 31, + 'unitsPerEm': 2048, + 'created': (0, 3514699492), 'modified': (0, 3514699492), + 'xMin': -2090, 'yMin': -948, 'xMax': 3673, 'yMax': 2524, + 'macStyle': 0, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 0, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 6241, + 'maxPoints': 852, 'maxComponentPoints': 104, 'maxTwilightPoints': 16, + 'maxContours': 43, 'maxComponentContours': 12, + 'maxZones': 2, + 'maxStorage': 153, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 1045, + 'maxSizeOfInstructions': 534, + 'maxComponentElements': 8, + 'maxComponentDepth': 4, + }, + 'OS/2': { + 'version': 1, + 'xAvgCharWidth': 1038, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 1331, 'ySubscriptYSize': 1433, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 286, + 'ySuperscriptXSize': 1331, 'ySuperscriptYSize': 1433, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 983, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04', + 'ulCharRange': (3875565311, 3523280383, 170156073, 67117068), + 'achVendID': b'PfEd', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 65535, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1901, 'descent': -483, 'lineGap': 0, + 'advanceWidthMax': 3838, + 'minLeftBearing': -2090, 'minRightBearing': -1455, + 'xMaxExtent': 3673, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 6226, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -130, 'underlineThickness': 90, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, + 'cmtt10': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 555110277, + 'magicNumber': 0x5F0F3CF5, + 'flags': 3, + 'unitsPerEm': 2048, + 'created': (0, 0), 'modified': (0, 0), + 'xMin': -12, 'yMin': -477, 'xMax': 1280, 'yMax': 1430, + 'macStyle': 0, + 'lowestRecPPEM': 6, + 'fontDirectionHint': 2, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 133, + 'maxPoints': 94, 'maxComponentPoints': 0, 'maxTwilightPoints': 12, + 'maxContours': 5, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 6, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 200, + 'maxSizeOfInstructions': 100, + 'maxComponentElements': 4, + 'maxComponentDepth': 1, + }, + 'OS/2': { + 'version': 0, + 'xAvgCharWidth': 1075, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 410, 'ySubscriptYSize': 369, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': -469, + 'ySuperscriptXSize': 410, 'ySuperscriptYSize': 369, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 1090, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (0, 0, 0, 0), + 'achVendID': b'\x00\x00\x00\x00', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 9835, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1276, 'descent': -489, 'lineGap': 0, + 'advanceWidthMax': 1536, + 'minLeftBearing': -12, 'minRightBearing': -29, + 'xMaxExtent': 1280, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 133, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -133, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': { + 'version': (1, 0), + 'fontNumber': 2147483648, + 'pitch': 1075, + 'xHeight': 905, + 'style': 0, + 'typeFamily': 0, + 'capHeight': 1276, + 'symbolSet': 0, + 'typeFace': b'cmtt10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'characterComplement': b'\xff\xff\xff\xff7\xff\xff\xfe', + 'strokeWeight': 0, + 'widthType': -5, + 'serifStyle': 64, + }, + }, + 'STIXSizeTwoSym:bold': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 1803408080, + 'magicNumber': 0x5F0F3CF5, + 'flags': 11, + 'unitsPerEm': 1000, + 'created': (0, 3359035786), 'modified': (0, 3359035786), + 'xMin': 4, 'yMin': -355, 'xMax': 1185, 'yMax': 2095, + 'macStyle': 1, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 2, + 'indexToLocFormat': 0, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 20, + 'maxPoints': 37, 'maxComponentPoints': 0, 'maxTwilightPoints': 0, + 'maxContours': 1, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 1, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 64, + 'maxSizeOfInstructions': 0, + 'maxComponentElements': 0, + 'maxComponentDepth': 0, + }, + 'OS/2': { + 'version': 2, + 'xAvgCharWidth': 598, + 'usWeightClass': 700, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 500, 'ySubscriptYSize': 500, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 250, + 'ySuperscriptXSize': 500, 'ySuperscriptYSize': 500, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 500, + 'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037, + 'sFamilyClass': 0, + 'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (3, 192, 0, 0), + 'achVendID': b'STIX', + 'fsSelection': 32, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 10217, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 2095, 'descent': -404, 'lineGap': 0, + 'advanceWidthMax': 1130, + 'minLeftBearing': 0, 'minRightBearing': -55, + 'xMaxExtent': 1185, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 19, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -123, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, +} + + +@pytest.mark.parametrize('font_name', _expected_sfnt_tables.keys()) +@pytest.mark.parametrize('header', _expected_sfnt_tables['DejaVu Sans'].keys()) +def test_ft2font_get_sfnt_table(font_name, header): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header] + + +@pytest.mark.parametrize('left, right, unscaled, unfitted, default', [ + # These are all the same class. + ('A', 'A', 57, 248, 256), ('A', 'À', 57, 248, 256), ('A', 'Á', 57, 248, 256), + ('A', 'Â', 57, 248, 256), ('A', 'Ã', 57, 248, 256), ('A', 'Ä', 57, 248, 256), + # And a few other random ones. + ('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024), + ('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512), +]) +def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): + file = fm.findfont('DejaVu Sans') + # With unscaled, these settings should produce exact values found in FontForge. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(100, 100) + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.Kerning.UNSCALED) == unscaled + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.Kerning.UNFITTED) == unfitted + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.Kerning.DEFAULT) == default + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.UNSCALED instead'): + k = ft2font.KERNING_UNSCALED + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == unscaled + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.UNFITTED instead'): + k = ft2font.KERNING_UNFITTED + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == unfitted + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.DEFAULT instead'): + k = ft2font.KERNING_DEFAULT + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == default + + +def test_ft2font_set_text(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + xys = font.set_text('') + np.testing.assert_array_equal(xys, np.empty((0, 2))) + assert font.get_width_height() == (0, 0) + assert font.get_num_glyphs() == 0 + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + # This string uses all the kerning pairs defined for test_ft2font_get_kerning. + xys = font.set_text('AADAT.XC-J') + np.testing.assert_array_equal( + xys, + [(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0), + (3200, 0), (3712, 0), (4032, 0)]) + assert font.get_width_height() == (4288, 768) + assert font.get_num_glyphs() == 10 + assert font.get_descent() == 192 + assert font.get_bitmap_offset() == (6, 0) + + +def test_ft2font_loading(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + for glyph in [font.load_char(ord('M')), + font.load_glyph(font.get_char_index(ord('M')))]: + assert glyph is not None + assert glyph.width == 576 + assert glyph.height == 576 + assert glyph.horiBearingX == 0 + assert glyph.horiBearingY == 576 + assert glyph.horiAdvance == 640 + assert glyph.linearHoriAdvance == 678528 + assert glyph.vertBearingX == -384 + assert glyph.vertBearingY == 64 + assert glyph.vertAdvance == 832 + assert glyph.bbox == (54, 0, 574, 576) + assert font.get_num_glyphs() == 2 # Both count as loaded. + # But neither has been placed anywhere. + assert font.get_width_height() == (0, 0) + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + + +def test_ft2font_drawing(): + expected_str = ( + ' ', + '11 11 ', + '11 11 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 11 1 ', + '1 11 1 ', + '1 1 ', + '1 1 ', + ' ', + ) + expected = np.array([ + [int(c) for c in line.replace(' ', '0')] for line in expected_str + ]) + expected *= 255 + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_text('M') + font.draw_glyphs_to_bitmap(antialiased=False) + image = font.get_image() + np.testing.assert_array_equal(image, expected) + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + glyph = font.load_char(ord('M')) + image = ft2font.FT2Image(expected.shape[1], expected.shape[0]) + font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) + np.testing.assert_array_equal(image, expected) + + +def test_ft2font_get_path(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + vertices, codes = font.get_path() + assert vertices.shape == (0, 2) + assert codes.shape == (0, ) + font.load_char(ord('M')) + vertices, codes = font.get_path() + expected_vertices = np.array([ + (0.843750, 9.000000), (2.609375, 9.000000), # Top left. + (4.906250, 2.875000), # Top of midpoint. + (7.218750, 9.000000), (8.968750, 9.000000), # Top right. + (8.968750, 0.000000), (7.843750, 0.000000), # Bottom right. + (7.843750, 7.906250), # Point under top right. + (5.531250, 1.734375), (4.296875, 1.734375), # Bar under midpoint. + (1.984375, 7.906250), # Point under top left. + (1.984375, 0.000000), (0.843750, 0.000000), # Bottom left. + (0.843750, 9.000000), # Back to top left corner. + (0.000000, 0.000000), + ]) + np.testing.assert_array_equal(vertices, expected_vertices) + expected_codes = np.full(expected_vertices.shape[0], mpath.Path.LINETO, + dtype=mpath.Path.code_type) + expected_codes[0] = mpath.Path.MOVETO + expected_codes[-1] = mpath.Path.CLOSEPOLY + np.testing.assert_array_equal(codes, expected_codes) @pytest.mark.parametrize('family_name, file_name', diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 0c032fa5367a..24a0ab929bbf 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -24,27 +24,6 @@ import pytest -@image_comparison(['image_interps'], style='mpl20') -def test_image_interps(): - """Make the basic nearest, bilinear and bicubic interps.""" - # Remove texts when this image is regenerated. - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - - X = np.arange(100).reshape(5, 20) - - fig, (ax1, ax2, ax3) = plt.subplots(3) - ax1.imshow(X, interpolation='nearest') - ax1.set_title('three interpolations') - ax1.set_ylabel('nearest') - - ax2.imshow(X, interpolation='bilinear') - ax2.set_ylabel('bilinear') - - ax3.imshow(X, interpolation='bicubic') - ax3.set_ylabel('bicubic') - - @image_comparison(['interp_alpha.png'], remove_text=True) def test_alpha_interp(): """Test the interpolation of the alpha channel on RGBA images""" @@ -119,7 +98,7 @@ def test_imshow_antialiased(fig_test, fig_ref, fig.set_size_inches(fig_size, fig_size) ax = fig_test.subplots() ax.set_position([0, 0, 1, 1]) - ax.imshow(A, interpolation='antialiased') + ax.imshow(A, interpolation='auto') ax = fig_ref.subplots() ax.set_position([0, 0, 1, 1]) ax.imshow(A, interpolation=interpolation) @@ -134,7 +113,7 @@ def test_imshow_zoom(fig_test, fig_ref): for fig in [fig_test, fig_ref]: fig.set_size_inches(2.9, 2.9) ax = fig_test.subplots() - ax.imshow(A, interpolation='antialiased') + ax.imshow(A, interpolation='auto') ax.set_xlim([10, 20]) ax.set_ylim([10, 20]) ax = fig_ref.subplots() @@ -447,15 +426,6 @@ def test_image_cliprect(): im.set_clip_path(rect) -@image_comparison(['imshow'], remove_text=True, style='mpl20') -def test_imshow(): - fig, ax = plt.subplots() - arr = np.arange(100).reshape((10, 10)) - ax.imshow(arr, interpolation="bilinear", extent=(1, 2, 1, 2)) - ax.set_xlim(0, 3) - ax.set_ylim(0, 3) - - @check_figures_equal(extensions=['png']) def test_imshow_10_10_1(fig_test, fig_ref): # 10x10x1 should be the same as 10x10 @@ -632,7 +602,7 @@ def test_bbox_image_inverted(): image = np.identity(10) bbox_im = BboxImage(TransformedBbox(Bbox([[0.1, 0.2], [0.3, 0.25]]), - ax.figure.transFigure), + ax.get_figure().transFigure), interpolation='nearest') bbox_im.set_data(image) bbox_im.set_clip_on(False) @@ -727,7 +697,7 @@ def test_jpeg_alpha(): # If this fails, there will be only one color (all black). If this # is working, we should have all 256 shades of grey represented. num_colors = len(image.getcolors(256)) - assert 175 <= num_colors <= 210 + assert 175 <= num_colors <= 230 # The fully transparent part should be red. corner_pixel = image.getpixel((0, 0)) assert corner_pixel == (254, 0, 0) @@ -884,8 +854,6 @@ def test_image_preserve_size2(): @image_comparison(['mask_image_over_under.png'], remove_text=True, tol=1.0) def test_mask_image_over_under(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False delta = 0.025 x = y = np.arange(-3.0, 3.0, delta) @@ -984,6 +952,7 @@ def test_imshow_masked_interpolation(): fig, ax_grid = plt.subplots(3, 6) interps = sorted(mimage._interpd_) + interps.remove('auto') interps.remove('antialiased') for interp, ax in zip(interps, ax_grid.ravel()): @@ -1415,9 +1384,28 @@ def test_nonuniform_and_pcolor(): ax.set(xlim=(0, 10)) -@image_comparison( - ['rgba_antialias.png'], style='mpl20', remove_text=True, - tol=0 if platform.machine() == 'x86_64' else 0.007) +@image_comparison(["nonuniform_logscale.png"], style="mpl20") +def test_nonuniform_logscale(): + _, axs = plt.subplots(ncols=3, nrows=1) + + for i in range(3): + ax = axs[i] + im = NonUniformImage(ax) + im.set_data(np.arange(1, 4) ** 2, np.arange(1, 4) ** 2, + np.arange(9).reshape((3, 3))) + ax.set_xlim(1, 16) + ax.set_ylim(1, 16) + ax.set_box_aspect(1) + if i == 1: + ax.set_xscale("log", base=2) + ax.set_yscale("log", base=2) + if i == 2: + ax.set_xscale("log", base=4) + ax.set_yscale("log", base=4) + ax.add_image(im) + + +@image_comparison(['rgba_antialias.png'], style='mpl20', remove_text=True, tol=0.02) def test_rgba_antialias(): fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, sharey=False, constrained_layout=True) @@ -1462,15 +1450,54 @@ def test_rgba_antialias(): # data antialias: Note no purples, and white in circle. Note # that alternating red and blue stripes become white. - axs[2].imshow(aa, interpolation='antialiased', interpolation_stage='data', + axs[2].imshow(aa, interpolation='auto', interpolation_stage='data', cmap=cmap, vmin=-1.2, vmax=1.2) # rgba antialias: Note purples at boundary with circle. Note that # alternating red and blue stripes become purple - axs[3].imshow(aa, interpolation='antialiased', interpolation_stage='rgba', + axs[3].imshow(aa, interpolation='auto', interpolation_stage='rgba', cmap=cmap, vmin=-1.2, vmax=1.2) +@check_figures_equal(extensions=('png', )) +def test_upsample_interpolation_stage(fig_test, fig_ref): + """ + Show that interpolation_stage='auto' gives the same as 'data' + for upsampling. + """ + # Fixing random state for reproducibility. This non-standard seed + # gives red splotches for 'rgba'. + np.random.seed(19680801+9) + + grid = np.random.rand(4, 4) + ax = fig_ref.subplots() + ax.imshow(grid, interpolation='bilinear', cmap='viridis', + interpolation_stage='data') + + ax = fig_test.subplots() + ax.imshow(grid, interpolation='bilinear', cmap='viridis', + interpolation_stage='auto') + + +@check_figures_equal(extensions=('png', )) +def test_downsample_interpolation_stage(fig_test, fig_ref): + """ + Show that interpolation_stage='auto' gives the same as 'rgba' + for downsampling. + """ + # Fixing random state for reproducibility + np.random.seed(19680801) + + grid = np.random.rand(4000, 4000) + ax = fig_ref.subplots() + ax.imshow(grid, interpolation='auto', cmap='viridis', + interpolation_stage='rgba') + + ax = fig_test.subplots() + ax.imshow(grid, interpolation='auto', cmap='viridis', + interpolation_stage='auto') + + def test_rc_interpolation_stage(): for val in ["data", "rgba"]: with mpl.rc_context({"image.interpolation_stage": val}): @@ -1588,6 +1615,87 @@ def test_non_transdata_image_does_not_touch_aspect(): assert ax.get_aspect() == 2 +@image_comparison( + ['downsampling.png'], style='mpl20', remove_text=True, tol=0.09) +def test_downsampling(): + N = 450 + x = np.arange(N) / N - 0.5 + y = np.arange(N) / N - 0.5 + aa = np.ones((N, N)) + aa[::2, :] = -1 + + X, Y = np.meshgrid(x, y) + R = np.sqrt(X**2 + Y**2) + f0 = 5 + k = 100 + a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) + # make the left hand side of this + a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1 + a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1 + aa[:, int(N / 3):] = a[:, int(N / 3):] + a = aa + + fig, axs = plt.subplots(2, 3, figsize=(7, 6), layout='compressed') + axs[0, 0].imshow(a, interpolation='nearest', interpolation_stage='rgba', + cmap='RdBu_r') + axs[0, 0].set_xlim(125, 175) + axs[0, 0].set_ylim(250, 200) + axs[0, 0].set_title('Zoom') + + for ax, interp, space in zip(axs.flat[1:], ['nearest', 'nearest', 'hanning', + 'hanning', 'auto'], + ['data', 'rgba', 'data', 'rgba', 'auto']): + ax.imshow(a, interpolation=interp, interpolation_stage=space, + cmap='RdBu_r') + ax.set_title(f"interpolation='{interp}'\nspace='{space}'") + + +@image_comparison( + ['downsampling_speckle.png'], style='mpl20', remove_text=True, tol=0.09) +def test_downsampling_speckle(): + fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), sharex=True, sharey=True, + layout="compressed") + axs = axs.flatten() + img = ((np.arange(1024).reshape(-1, 1) * np.ones(720)) // 50).T + + cm = plt.get_cmap("viridis") + cm.set_over("m") + norm = colors.LogNorm(vmin=3, vmax=11) + + # old default cannot be tested because it creates over/under speckles + # in the following that are machine dependent. + + axs[0].set_title("interpolation='auto', stage='rgba'") + axs[0].imshow(np.triu(img), cmap=cm, norm=norm, interpolation_stage='rgba') + + # Should be same as previous + axs[1].set_title("interpolation='auto', stage='auto'") + axs[1].imshow(np.triu(img), cmap=cm, norm=norm) + + +@image_comparison( + ['upsampling.png'], style='mpl20', remove_text=True) +def test_upsampling(): + + np.random.seed(19680801+9) # need this seed to get yellow next to blue + a = np.random.rand(4, 4) + + fig, axs = plt.subplots(1, 3, figsize=(6.5, 3), layout='compressed') + im = axs[0].imshow(a, cmap='viridis') + axs[0].set_title( + "interpolation='auto'\nstage='antialaised'\n(default for upsampling)") + + # probably what people want: + axs[1].imshow(a, cmap='viridis', interpolation='sinc') + axs[1].set_title( + "interpolation='sinc'\nstage='auto'\n(default for upsampling)") + + # probably not what people want: + axs[2].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='rgba') + axs[2].set_title("interpolation='sinc'\nstage='rgba'") + fig.colorbar(im, ax=axs, shrink=0.7, extend='both') + + @pytest.mark.parametrize( 'dtype', ('float64', 'float32', 'int16', 'uint16', 'int8', 'uint8'), diff --git a/lib/matplotlib/tests/test_inset.py b/lib/matplotlib/tests/test_inset.py new file mode 100644 index 000000000000..c25580214c80 --- /dev/null +++ b/lib/matplotlib/tests/test_inset.py @@ -0,0 +1,106 @@ +import platform + +import pytest + +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +from matplotlib.testing.decorators import image_comparison, check_figures_equal + + +def test_indicate_inset_no_args(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='At least one of bounds or inset_ax'): + ax.indicate_inset() + + +@check_figures_equal(extensions=["png"]) +def test_zoom_inset_update_limits(fig_test, fig_ref): + # Updating the inset axes limits should also update the indicator #19768 + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + for ax in ax_ref, ax_test: + ax.set_xlim([0, 5]) + ax.set_ylim([0, 5]) + + inset_ref = ax_ref.inset_axes([0.6, 0.6, 0.3, 0.3]) + inset_test = ax_test.inset_axes([0.6, 0.6, 0.3, 0.3]) + + inset_ref.set_xlim([1, 2]) + inset_ref.set_ylim([3, 4]) + ax_ref.indicate_inset_zoom(inset_ref) + + ax_test.indicate_inset_zoom(inset_test) + inset_test.set_xlim([1, 2]) + inset_test.set_ylim([3, 4]) + + +def test_inset_indicator_update_styles(): + fig, ax = plt.subplots() + inset = ax.inset_axes([0.6, 0.6, 0.3, 0.3]) + inset.set_xlim([0.2, 0.4]) + inset.set_ylim([0.2, 0.4]) + + indicator = ax.indicate_inset_zoom( + inset, edgecolor='red', alpha=0.5, linewidth=2, linestyle='solid') + + # Changing the rectangle styles should not affect the connectors. + indicator.rectangle.set(color='blue', linestyle='dashed', linewidth=42, alpha=0.2) + for conn in indicator.connectors: + assert mcolors.same_color(conn.get_edgecolor()[:3], 'red') + assert conn.get_alpha() == 0.5 + assert conn.get_linestyle() == 'solid' + assert conn.get_linewidth() == 2 + + # Changing the indicator styles should affect both rectangle and connectors. + indicator.set(color='green', linestyle='dotted', linewidth=7, alpha=0.8) + assert mcolors.same_color(indicator.rectangle.get_facecolor()[:3], 'green') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'green') + assert patch.get_alpha() == 0.8 + assert patch.get_linestyle() == 'dotted' + assert patch.get_linewidth() == 7 + + indicator.set_edgecolor('purple') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'purple') + + # This should also be true if connectors weren't created yet. + indicator._connectors = [] + indicator.set(color='burlywood', linestyle='dashdot', linewidth=4, alpha=0.4) + assert mcolors.same_color(indicator.rectangle.get_facecolor()[:3], 'burlywood') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'burlywood') + assert patch.get_alpha() == 0.4 + assert patch.get_linestyle() == 'dashdot' + assert patch.get_linewidth() == 4 + + indicator._connectors = [] + indicator.set_edgecolor('thistle') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'thistle') + + +def test_inset_indicator_zorder(): + fig, ax = plt.subplots() + rect = [0.2, 0.2, 0.3, 0.4] + + inset = ax.indicate_inset(rect) + assert inset.get_zorder() == 4.99 + + inset = ax.indicate_inset(rect, zorder=42) + assert inset.get_zorder() == 42 + + +@image_comparison(['zoom_inset_connector_styles.png'], remove_text=True, style='mpl20', + tol=0.024 if platform.machine() == 'arm64' else 0) +def test_zoom_inset_connector_styles(): + fig, axs = plt.subplots(2) + for ax in axs: + ax.plot([1, 2, 3]) + + axs[1].set_xlim(0.5, 1.5) + indicator = axs[0].indicate_inset_zoom(axs[1], linewidth=5) + # Make one visible connector a different style + indicator.connectors[1].set_linestyle('dashed') + indicator.connectors[1].set_color('blue') diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 3c2af275649f..61892378bd03 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -19,7 +19,7 @@ import matplotlib.lines as mlines from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend -from matplotlib import _api, rc_context +from matplotlib import rc_context from matplotlib.font_manager import FontProperties @@ -138,19 +138,6 @@ def test_various_labels(): ax.legend(numpoints=1, loc='best') -def test_legend_label_with_leading_underscore(): - """ - Test that artists with labels starting with an underscore are not added to - the legend, and that a warning is issued if one tries to add them - explicitly. - """ - fig, ax = plt.subplots() - line, = ax.plot([0, 1], label='_foo') - with pytest.warns(_api.MatplotlibDeprecationWarning, match="with an underscore"): - legend = ax.legend(handles=[line]) - assert len(legend.legend_handles) == 0 - - @image_comparison(['legend_labels_first.png'], remove_text=True, tol=0.013 if platform.machine() == 'arm64' else 0) def test_labels_first(): @@ -415,7 +402,7 @@ def test_warn_mixed_args_and_kwargs(self): "be discarded.") def test_parasite(self): - from mpl_toolkits.axes_grid1 import host_subplot # type: ignore + from mpl_toolkits.axes_grid1 import host_subplot # type: ignore[import] host = host_subplot(111) par = host.twinx() @@ -1259,7 +1246,7 @@ def test_subfigure_legend(): ax = subfig.subplots() ax.plot([0, 1], [0, 1], label="line") leg = subfig.legend() - assert leg.figure is subfig + assert leg.get_figure(root=False) is subfig def test_setting_alpha_keeps_polycollection_color(): diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 902b7aa2c02d..ee8b5b4aaa9e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -417,16 +417,20 @@ def test_axline_setters(): line2 = ax.axline((.1, .1), (.8, .4)) # Testing xy1, xy2 and slope setters. # This should not produce an error. - line1.set_xy1(.2, .3) + line1.set_xy1((.2, .3)) line1.set_slope(2.4) - line2.set_xy1(.3, .2) - line2.set_xy2(.6, .8) + line2.set_xy1((.3, .2)) + line2.set_xy2((.6, .8)) # Testing xy1, xy2 and slope getters. # Should return the modified values. assert line1.get_xy1() == (.2, .3) assert line1.get_slope() == 2.4 assert line2.get_xy1() == (.3, .2) assert line2.get_xy2() == (.6, .8) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line1.set_xy1(.2, .3) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line2.set_xy2(.6, .8) # Testing setting xy2 and slope together. # These test should raise a ValueError with pytest.raises(ValueError, diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index e3659245d0e7..4dcd08ba0718 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -4,7 +4,6 @@ from pathlib import Path import platform import re -import shlex from xml.etree import ElementTree as ET from typing import Any @@ -124,6 +123,7 @@ r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799 r'$\left(X\right)_{a}^{b}$', # github issue 7615 r'$\dfrac{\$100.00}{y}$', # github issue #1888 + r'$a=-b-c$' # github issue #28180 ] # 'svgastext' tests switch svg output to embed text as text (rather than as # paths). @@ -197,8 +197,8 @@ *('}' for font in fonts), '$', ]) - for set in chars: - font_tests.append(wrapper % set) + for font_set in chars: + font_tests.append(wrapper % font_set) @pytest.fixture @@ -270,7 +270,7 @@ def test_short_long_accents(fig_test, fig_ref): short_accs = [s for s in acc_map if len(s) == 1] corresponding_long_accs = [] for s in short_accs: - l, = [l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s]] + l, = (l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s]) corresponding_long_accs.append(l) fig_test.text(0, .5, "$" + "".join(rf"\{s}a" for s in short_accs) + "$") fig_ref.text( @@ -432,7 +432,7 @@ def test_mathtext_fallback_invalid(): @pytest.mark.parametrize( "fallback,fontlist", [("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']), - ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])]) + ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'STIXGeneral', 'STIXGeneral'])]) def test_mathtext_fallback(fallback, fontlist): mpl.font_manager.fontManager.addfont( str(Path(__file__).resolve().parent / 'mpltest.ttf')) @@ -452,10 +452,10 @@ def test_mathtext_fallback(fallback, fontlist): fig.savefig(buff, format="svg") tspans = (ET.fromstring(buff.getvalue()) .findall(".//{http://www.w3.org/2000/svg}tspan[@style]")) - # Getting the last element of the style attrib is a close enough - # approximation for parsing the font property. - char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans] - assert char_fonts == fontlist + char_fonts = [ + re.search(r"font-family: '([\w ]+)'", tspan.attrib["style"]).group(1) + for tspan in tspans] + assert char_fonts == fontlist, f'Expected {fontlist}, got {char_fonts}' mpl.font_manager.fontManager.ttflist.pop() diff --git a/lib/matplotlib/tests/test_multivariate_colormaps.py b/lib/matplotlib/tests/test_multivariate_colormaps.py new file mode 100644 index 000000000000..81a2e6adeb35 --- /dev/null +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -0,0 +1,564 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose +import matplotlib.pyplot as plt +from matplotlib.testing.decorators import (image_comparison, + remove_ticks_and_titles) +import matplotlib as mpl +import pytest +from pathlib import Path +from io import BytesIO +from PIL import Image +import base64 + + +@image_comparison(["bivariate_cmap_shapes.png"]) +def test_bivariate_cmap_shapes(): + x_0 = np.repeat(np.linspace(-0.1, 1.1, 10, dtype='float32')[None, :], 10, axis=0) + x_1 = x_0.T + + fig, axes = plt.subplots(1, 4, figsize=(10, 2)) + + # shape = 'square' + cmap = mpl.bivar_colormaps['BiPeak'] + axes[0].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = 'circle' + cmap = mpl.bivar_colormaps['BiCone'] + axes[1].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = 'ignore' + cmap = mpl.bivar_colormaps['BiPeak'] + cmap = cmap.with_extremes(shape='ignore') + axes[2].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = circleignore + cmap = mpl.bivar_colormaps['BiCone'] + cmap = cmap.with_extremes(shape='circleignore') + axes[3].imshow(cmap((x_0, x_1)), interpolation='nearest') + remove_ticks_and_titles(fig) + + +def test_multivar_creation(): + # test creation of a custom multivariate colorbar + blues = mpl.colormaps['Blues'] + cmap = mpl.colors.MultivarColormap((blues, 'Oranges'), 'sRGB_sub') + y, x = np.mgrid[0:3, 0:3]/2 + im = cmap((y, x)) + res = np.array([[[0.96862745, 0.94509804, 0.92156863, 1], + [0.96004614, 0.53504037, 0.23277201, 1], + [0.46666667, 0.1372549, 0.01568627, 1]], + [[0.41708574, 0.64141484, 0.75980008, 1], + [0.40850442, 0.23135717, 0.07100346, 1], + [0, 0, 0, 1]], + [[0.03137255, 0.14901961, 0.34117647, 1], + [0.02279123, 0, 0, 1], + [0, 0, 0, 1]]]) + assert_allclose(im, res, atol=0.01) + + with pytest.raises(ValueError, match="colormaps must be a list of"): + cmap = mpl.colors.MultivarColormap((blues, [blues]), 'sRGB_sub') + with pytest.raises(ValueError, match="A MultivarColormap must"): + cmap = mpl.colors.MultivarColormap('blues', 'sRGB_sub') + with pytest.raises(ValueError, match="A MultivarColormap must"): + cmap = mpl.colors.MultivarColormap((blues), 'sRGB_sub') + + +@image_comparison(["multivar_alpha_mixing.png"]) +def test_multivar_alpha_mixing(): + # test creation of a custom colormap using 'rainbow' + # and a colormap that goes from alpha = 1 to alpha = 0 + rainbow = mpl.colormaps['rainbow'] + alpha = np.zeros((256, 4)) + alpha[:, 3] = np.linspace(1, 0, 256) + alpha_cmap = mpl.colors.LinearSegmentedColormap.from_list('from_list', alpha) + + cmap = mpl.colors.MultivarColormap((rainbow, alpha_cmap), 'sRGB_add') + y, x = np.mgrid[0:10, 0:10]/9 + im = cmap((y, x)) + + fig, ax = plt.subplots() + ax.imshow(im, interpolation='nearest') + remove_ticks_and_titles(fig) + + +def test_multivar_cmap_call(): + cmap = mpl.multivar_colormaps['2VarAddA'] + assert_array_equal(cmap((0.0, 0.0)), (0, 0, 0, 1)) + assert_array_equal(cmap((1.0, 1.0)), (1, 1, 1, 1)) + assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1) + + cmap = mpl.multivar_colormaps['2VarSubA'] + assert_array_equal(cmap((0.0, 0.0)), (1, 1, 1, 1)) + assert_allclose(cmap((1.0, 1.0)), (0, 0, 0, 1), atol=0.1) + + # check outside and bad + cs = cmap([(0., 0., 0., 1.2, np.nan), (0., 1.2, np.nan, 0., 0., )]) + assert_allclose(cs, [[1., 1., 1., 1.], + [0.801, 0.426, 0.119, 1.], + [0., 0., 0., 0.], + [0.199, 0.574, 0.881, 1.], + [0., 0., 0., 0.]]) + + assert_array_equal(cmap((0.0, 0.0), bytes=True), (255, 255, 255, 255)) + + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], alpha=(0.5, 0.3)) + + with pytest.raises(ValueError, match="For the selected colormap the data"): + cs = cmap([(0, 5, 9), (0, 0, 0), (0, 0, 0)]) + + with pytest.raises(ValueError, match="clip cannot be false"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, clip=False) + # Tests calling a multivariate colormap with integer values + cmap = mpl.multivar_colormaps['2VarSubA'] + + # call only integers + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)]) + res = np.array([[1, 1, 1, 1], + [0.85176471, 0.91029412, 0.96023529, 1], + [0.70452941, 0.82764706, 0.93358824, 1], + [0.94358824, 0.88505882, 0.83511765, 1], + [0.89729412, 0.77417647, 0.66823529, 1], + [0, 0, 0, 1]]) + assert_allclose(cs, res, atol=0.01) + + # call only integers, wrong byte order + swapped_dt = np.dtype(int).newbyteorder() + cs = cmap([np.array([0, 50, 100, 0, 0, 300], dtype=swapped_dt), + np.array([0, 0, 0, 50, 100, 300], dtype=swapped_dt)]) + assert_allclose(cs, res, atol=0.01) + + # call mix floats integers + # check calling with bytes = True + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], bytes=True) + res = np.array([[255, 255, 255, 255], + [217, 232, 244, 255], + [179, 211, 238, 255], + [240, 225, 212, 255], + [228, 197, 170, 255], + [0, 0, 0, 255]]) + assert_allclose(cs, res, atol=0.01) + + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], alpha=0.5) + res = np.array([[1, 1, 1, 0.5], + [0.85176471, 0.91029412, 0.96023529, 0.5], + [0.70452941, 0.82764706, 0.93358824, 0.5], + [0.94358824, 0.88505882, 0.83511765, 0.5], + [0.89729412, 0.77417647, 0.66823529, 0.5], + [0, 0, 0, 0.5]]) + assert_allclose(cs, res, atol=0.01) + # call with tuple + assert_allclose(cmap((100, 120), bytes=True, alpha=0.5), + [149, 142, 136, 127], atol=0.01) + + # alpha and bytes + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5) + res = np.array([[0, 0, 255, 127], + [141, 0, 255, 127], + [255, 0, 255, 127], + [0, 115, 255, 127], + [0, 255, 255, 127], + [255, 255, 255, 127]]) + + # bad alpha shape + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3)) + + cmap = cmap.with_extremes(bad=(1, 1, 1, 1)) + cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)]) + res = np.array([[1., 1., 1., 1.], + [0., 0., 0., 1.], + [1., 1., 1., 1.]]) + assert_allclose(cs, res, atol=0.01) + + # call outside with tuple + assert_allclose(cmap((300, 300), bytes=True, alpha=0.5), + [0, 0, 0, 127], atol=0.01) + with pytest.raises(ValueError, + match="For the selected colormap the data must have"): + cs = cmap((0, 5, 9)) + + # test over/under + cmap = mpl.multivar_colormaps['2VarAddA'] + with pytest.raises(ValueError, match='i.e. be of length 2'): + cmap.with_extremes(over=0) + with pytest.raises(ValueError, match='i.e. be of length 2'): + cmap.with_extremes(under=0) + + cmap = cmap.with_extremes(under=[(0, 0, 0, 0)]*2) + assert_allclose((0, 0, 0, 0), cmap((-1., 0)), atol=1e-2) + cmap = cmap.with_extremes(over=[(0, 0, 0, 0)]*2) + assert_allclose((0, 0, 0, 0), cmap((2., 0)), atol=1e-2) + + +def test_multivar_bad_mode(): + cmap = mpl.multivar_colormaps['2VarSubA'] + with pytest.raises(ValueError, match="is not a valid value for"): + cmap = mpl.colors.MultivarColormap(cmap[:], 'bad') + + +def test_multivar_resample(): + cmap = mpl.multivar_colormaps['3VarAddA'] + cmap_resampled = cmap.resampled((None, 10, 3)) + + assert_allclose(cmap_resampled[1](0.25), (0.093, 0.116, 0.059, 1.0)) + assert_allclose(cmap_resampled((0, 0.25, 0)), (0.093, 0.116, 0.059, 1.0)) + assert_allclose(cmap_resampled((1, 0.25, 1)), (0.417271, 0.264624, 0.274976, 1.), + atol=0.01) + + with pytest.raises(ValueError, match="lutshape must be of length"): + cmap = cmap.resampled(4) + + +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) + + +def test_bivar_cmap_call(): + """ + Tests calling a bivariate colormap with integer values + """ + im = np.ones((10, 12, 4)) + im[:, :, 0] = np.linspace(0, 1, 10)[:, np.newaxis] + im[:, :, 1] = np.linspace(0, 1, 12)[np.newaxis, :] + cmap = mpl.colors.BivarColormapFromImage(im) + + # call only integers + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)]) + res = np.array([[0, 0, 1, 1], + [0.556, 0, 1, 1], + [1, 0, 1, 1], + [0, 0.454, 1, 1], + [0, 1, 1, 1], + [1, 1, 1, 1]]) + assert_allclose(cs, res, atol=0.01) + # call only integers, wrong byte order + swapped_dt = np.dtype(int).newbyteorder() + cs = cmap([np.array([0, 5, 9, 0, 0, 10], dtype=swapped_dt), + np.array([0, 0, 0, 5, 11, 12], dtype=swapped_dt)]) + assert_allclose(cs, res, atol=0.01) + + # call mix floats integers + cmap = cmap.with_extremes(outside=(1, 0, 0, 0)) + cs = cmap([(0.5, 0), (0, 3)]) + res = np.array([[0.555, 0, 1, 1], + [0, 0.2727, 1, 1]]) + assert_allclose(cs, res, atol=0.01) + + # check calling with bytes = True + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True) + res = np.array([[0, 0, 255, 255], + [141, 0, 255, 255], + [255, 0, 255, 255], + [0, 115, 255, 255], + [0, 255, 255, 255], + [255, 255, 255, 255]]) + assert_allclose(cs, res, atol=0.01) + + # test alpha + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], alpha=0.5) + res = np.array([[0, 0, 1, 0.5], + [0.556, 0, 1, 0.5], + [1, 0, 1, 0.5], + [0, 0.454, 1, 0.5], + [0, 1, 1, 0.5], + [1, 1, 1, 0.5]]) + assert_allclose(cs, res, atol=0.01) + # call with tuple + assert_allclose(cmap((10, 12), bytes=True, alpha=0.5), + [255, 255, 255, 127], atol=0.01) + + # alpha and bytes + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5) + res = np.array([[0, 0, 255, 127], + [141, 0, 255, 127], + [255, 0, 255, 127], + [0, 115, 255, 127], + [0, 255, 255, 127], + [255, 255, 255, 127]]) + + # bad alpha shape + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3)) + + # set shape to 'ignore'. + # final point is outside colormap and should then receive + # the 'outside' (in this case [1,0,0,0]) + # also test 'bad' (in this case [1,1,1,0]) + cmap = cmap.with_extremes(outside=(1, 0, 0, 0), bad=(1, 1, 1, 0), shape='ignore') + cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)]) + res = np.array([[0, 0, 1, 1], + [1, 0, 0, 0], + [1, 1, 1, 0]]) + assert_allclose(cs, res, atol=0.01) + # call outside with tuple + assert_allclose(cmap((10, 12), bytes=True, alpha=0.5), + [255, 0, 0, 127], atol=0.01) + # with integers + cs = cmap([(0, 10), (0, 12)]) + res = np.array([[0, 0, 1, 1], + [1, 0, 0, 0]]) + assert_allclose(cs, res, atol=0.01) + + with pytest.raises(ValueError, + match="For a `BivarColormap` the data must have"): + cs = cmap((0, 5, 9)) + + cmap = cmap.with_extremes(shape='circle') + with pytest.raises(NotImplementedError, + 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)) + with pytest.raises(KeyError, + match="only 0 or 1 are valid keys"): + cs = cmap[2] + + +def test_bivar_getitem(): + """Test __getitem__ on BivarColormap""" + xA = ([.0, .25, .5, .75, 1., -1, 2], [.5]*7) + xB = ([.5]*7, [.0, .25, .5, .75, 1., -1, 2]) + + cmaps = mpl.bivar_colormaps['BiPeak'] + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + cmaps = cmaps.with_extremes(shape='ignore') + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + xA = ([.0, .25, .5, .75, 1., -1, 2], [.0]*7) + xB = ([.0]*7, [.0, .25, .5, .75, 1., -1, 2]) + cmaps = mpl.bivar_colormaps['BiOrangeBlue'] + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + cmaps = cmaps.with_extremes(shape='ignore') + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + +def test_bivar_cmap_bad_shape(): + """ + Tests calling a bivariate colormap with integer values + """ + cmap = mpl.bivar_colormaps['BiCone'] + _ = cmap.lut + with pytest.raises(ValueError, + match="is not a valid value for shape"): + cmap.with_extremes(shape='bad_shape') + + with pytest.raises(ValueError, + match="is not a valid value for shape"): + mpl.colors.BivarColormapFromImage(np.ones((3, 3, 4)), + shape='bad_shape') + + +def test_bivar_cmap_bad_lut(): + """ + Tests calling a bivariate colormap with integer values + """ + with pytest.raises(ValueError, + match="The lut must be an array of shape"): + cmap = mpl.colors.BivarColormapFromImage(np.ones((3, 3, 5))) + + +def test_bivar_cmap_from_image(): + """ + This tests the creation and use of a bivariate colormap + generated from an image + """ + + data_0 = np.arange(6).reshape((2, 3))/5 + data_1 = np.arange(6).reshape((3, 2)).T/5 + + # bivariate colormap from array + cim = np.ones((10, 12, 3)) + cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10 + cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12 + + cmap = mpl.colors.BivarColormapFromImage(cim) + im = cmap((data_0, data_1)) + res = np.array([[[0, 0, 1, 1], + [0.2, 0.33333333, 1, 1], + [0.4, 0.75, 1, 1]], + [[0.6, 0.16666667, 1, 1], + [0.8, 0.58333333, 1, 1], + [0.9, 0.91666667, 1, 1]]]) + assert_allclose(im, res, atol=0.01) + + # input as unit8 + cim = np.ones((10, 12, 3))*255 + cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10*255 + cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12*255 + + cmap = mpl.colors.BivarColormapFromImage(cim.astype(np.uint8)) + im = cmap((data_0, data_1)) + res = np.array([[[0, 0, 1, 1], + [0.2, 0.33333333, 1, 1], + [0.4, 0.75, 1, 1]], + [[0.6, 0.16666667, 1, 1], + [0.8, 0.58333333, 1, 1], + [0.9, 0.91666667, 1, 1]]]) + assert_allclose(im, res, atol=0.01) + + # bivariate colormap from array + png_path = Path(__file__).parent / "baseline_images/pngsuite/basn2c16.png" + cim = Image.open(png_path) + cim = np.asarray(cim.convert('RGBA')) + + cmap = mpl.colors.BivarColormapFromImage(cim) + im = cmap((data_0, data_1), bytes=True) + res = np.array([[[255, 255, 0, 255], + [156, 206, 0, 255], + [49, 156, 49, 255]], + [[206, 99, 0, 255], + [99, 49, 107, 255], + [0, 0, 255, 255]]]) + assert_allclose(im, res, atol=0.01) + + +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'].resampled((2, -2)) + assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2)) + assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2) + + 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) + + with pytest.raises(ValueError, match="lutshape must be of length"): + cmap = cmap.resampled(4) + + +def test_bivariate_repr_png(): + cmap = mpl.bivar_colormaps['BiCone'] + png = cmap._repr_png_() + assert len(png) > 0 + img = Image.open(BytesIO(png)) + assert img.width > 0 + assert img.height > 0 + assert 'Title' in img.text + assert 'Description' in img.text + assert 'Author' in img.text + assert 'Software' in img.text + + +def test_bivariate_repr_html(): + cmap = mpl.bivar_colormaps['BiCone'] + html = cmap._repr_html_() + assert len(html) > 0 + png = cmap._repr_png_() + assert base64.b64encode(png).decode('ascii') in html + assert cmap.name in html + assert html.startswith('') + + +def test_multivariate_repr_png(): + cmap = mpl.multivar_colormaps['3VarAddA'] + png = cmap._repr_png_() + assert len(png) > 0 + img = Image.open(BytesIO(png)) + assert img.width > 0 + assert img.height > 0 + assert 'Title' in img.text + assert 'Description' in img.text + assert 'Author' in img.text + assert 'Software' in img.text + + +def test_multivariate_repr_html(): + cmap = mpl.multivar_colormaps['3VarAddA'] + html = cmap._repr_html_() + assert len(html) > 0 + for c in cmap: + png = c._repr_png_() + assert base64.b64encode(png).decode('ascii') in html + assert cmap.name in html + assert html.startswith('') + + +def test_bivar_eq(): + """ + Tests equality between multivariate colormaps + """ + cmap_0 = mpl.bivar_colormaps['BiPeak'] + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + assert (cmap_0 == cmap_1) is True + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiCone'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(bad='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(outside='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1._init() + cmap_1._lut *= 0.5 + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(shape='ignore') + assert (cmap_0 == cmap_1) is False + + +def test_multivar_eq(): + """ + Tests equality between multivariate colormaps + """ + cmap_0 = mpl.multivar_colormaps['2VarAddA'] + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + assert (cmap_0 == cmap_1) is True + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.colors.MultivarColormap([cmap_0[0]]*2, + 'sRGB_add') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['3VarAddA'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + cmap_1 = cmap_1.with_extremes(bad='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + cmap_1 = mpl.colors.MultivarColormap(cmap_1[:], 'sRGB_sub') + assert (cmap_0 == cmap_1) is False diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 3544ce8cb10c..9cb67616a96b 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -606,6 +606,28 @@ def test_connection_patch_fig(fig_test, fig_ref): fig_ref.add_artist(con) +@check_figures_equal(extensions=["png"]) +def test_connection_patch_pixel_points(fig_test, fig_ref): + xyA_pts = (.3, .2) + xyB_pts = (-30, -20) + + ax1, ax2 = fig_test.subplots(1, 2) + con = mpatches.ConnectionPatch(xyA=xyA_pts, coordsA="axes points", axesA=ax1, + xyB=xyB_pts, coordsB="figure points", + arrowstyle="->", shrinkB=5) + fig_test.add_artist(con) + + plt.rcParams["savefig.dpi"] = plt.rcParams["figure.dpi"] + + ax1, ax2 = fig_ref.subplots(1, 2) + xyA_pix = (xyA_pts[0]*(fig_ref.dpi/72), xyA_pts[1]*(fig_ref.dpi/72)) + xyB_pix = (xyB_pts[0]*(fig_ref.dpi/72), xyB_pts[1]*(fig_ref.dpi/72)) + con = mpatches.ConnectionPatch(xyA=xyA_pix, coordsA="axes pixels", axesA=ax1, + xyB=xyB_pix, coordsB="figure pixels", + arrowstyle="->", shrinkB=5) + fig_ref.add_artist(con) + + def test_datetime_rectangle(): # Check that creating a rectangle with timedeltas doesn't fail from datetime import datetime, timedelta @@ -960,3 +982,20 @@ def test_arrow_set_data(): ) arrow.set_data(x=.5, dx=3, dy=8, width=1.2) assert np.allclose(expected2, np.round(arrow.get_verts(), 2)) + + +@check_figures_equal(extensions=["png", "pdf", "svg", "eps"]) +def test_set_and_get_hatch_linewidth(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + lw = 2.0 + + with plt.rc_context({"hatch.linewidth": lw}): + ax_ref.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x")) + + ax_test.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x")) + ax_test.patches[0].set_hatch_linewidth(lw) + + assert ax_ref.patches[0].get_hatch_linewidth() == lw + assert ax_test.patches[0].get_hatch_linewidth() == lw diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 1474a67d28aa..eb47b2668101 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -17,7 +17,7 @@ import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms import matplotlib.figure as mfigure -from mpl_toolkits.axes_grid1 import axes_divider, parasite_axes # type: ignore +from mpl_toolkits.axes_grid1 import axes_divider, parasite_axes # type: ignore[import] def test_simple(): @@ -150,7 +150,15 @@ 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'} + 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__, + }, ) loaded_fig = pickle.loads(ast.literal_eval(proc.stdout)) diff --git a/lib/matplotlib/tests/test_png.py b/lib/matplotlib/tests/test_png.py index 066eb01c3ae6..9208c31df2bf 100644 --- a/lib/matplotlib/tests/test_png.py +++ b/lib/matplotlib/tests/test_png.py @@ -7,7 +7,7 @@ from matplotlib import cm, pyplot as plt -@image_comparison(['pngsuite.png'], tol=0.03) +@image_comparison(['pngsuite.png'], tol=0.04) def test_pngsuite(): files = sorted( (Path(__file__).parent / "baseline_images/pngsuite").glob("basn*.png")) @@ -20,7 +20,10 @@ def test_pngsuite(): if data.ndim == 2: # keep grayscale images gray cmap = cm.gray - plt.imshow(data, extent=(i, i + 1, 0, 1), cmap=cmap) + # Using the old default data interpolation stage lets us + # continue to use the existing reference image + plt.imshow(data, extent=(i, i + 1, 0, 1), cmap=cmap, + interpolation_stage='data') plt.gca().patch.set_facecolor("#ddffff") plt.gca().set_xlim(0, len(files)) diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 6b3c08d2eb3f..ee38c88a123f 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -436,6 +436,33 @@ def test_cursor_precision(): assert ax.format_coord(2, 1) == "θ=0.637π (114.6°), r=1.000" +def test_custom_fmt_data(): + ax = plt.subplot(projection="polar") + def millions(x): + return '$%1.1fM' % (x*1e-6) + + # Test only x formatter + ax.fmt_xdata = None + ax.fmt_ydata = millions + assert ax.format_coord(12, 2e7) == "θ=3.8197186342π (687.54935416°), r=$20.0M" + assert ax.format_coord(1234, 2e6) == "θ=392.794399551π (70702.9919191°), r=$2.0M" + assert ax.format_coord(3, 100) == "θ=0.95493π (171.887°), r=$0.0M" + + # Test only y formatter + ax.fmt_xdata = millions + ax.fmt_ydata = None + assert ax.format_coord(2e5, 1) == "θ=$0.2M, r=1.000" + assert ax.format_coord(1, .1) == "θ=$0.0M, r=0.100" + assert ax.format_coord(1e6, 0.005) == "θ=$1.0M, r=0.005" + + # Test both x and y formatters + ax.fmt_xdata = millions + ax.fmt_ydata = millions + assert ax.format_coord(2e6, 2e4*3e5) == "θ=$2.0M, r=$6000.0M" + assert ax.format_coord(1e18, 12891328123) == "θ=$1000000000000.0M, r=$12891.3M" + assert ax.format_coord(63**7, 1081968*1024) == "θ=$3938980.6M, r=$1107.9M" + + @image_comparison(['polar_log.png'], style='default') def test_polar_log(): fig = plt.figure() @@ -454,3 +481,21 @@ def test_polar_neg_theta_lims(): ax.set_thetalim(-np.pi, np.pi) labels = [l.get_text() for l in ax.xaxis.get_ticklabels()] assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°'] + + +@pytest.mark.parametrize("order", ["before", "after"]) +@image_comparison(baseline_images=['polar_errorbar'], remove_text=True, + extensions=['png'], style='mpl20') +def test_polar_errorbar(order): + theta = np.arange(0, 2 * np.pi, np.pi / 8) + r = theta / np.pi / 2 + 0.5 + fig = plt.figure(figsize=(5, 5)) + ax = fig.add_subplot(projection='polar') + if order == "before": + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen") + else: + ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen") + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index a077aede8f8b..21036e177045 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -439,9 +439,8 @@ def test_switch_backend_no_close(): assert len(plt.get_fignums()) == 2 plt.switch_backend('agg') assert len(plt.get_fignums()) == 2 - with pytest.warns(mpl.MatplotlibDeprecationWarning): - plt.switch_backend('svg') - assert len(plt.get_fignums()) == 0 + plt.switch_backend('svg') + assert len(plt.get_fignums()) == 2 def figure_hook_example(figure): @@ -457,3 +456,22 @@ def test_figure_hook(): fig = plt.figure() assert fig._test_was_here + + +def test_multiple_same_figure_calls(): + fig = mpl.pyplot.figure(1, figsize=(1, 2)) + with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"): + fig2 = mpl.pyplot.figure(1, figsize=(3, 4)) + with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"): + mpl.pyplot.figure(fig, figsize=(5, 6)) + assert fig is fig2 + fig3 = mpl.pyplot.figure(1) # Checks for false warnings + assert fig is fig3 + + +def test_close_all_warning(): + fig1 = plt.figure() + + # Check that the warning is issued when 'all' is passed to plt.figure + with pytest.warns(UserWarning, match="closes all existing figures"): + fig2 = plt.figure("all") diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index 7c5a9d343530..e28b04025b5e 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -6,6 +6,7 @@ from matplotlib import pyplot as plt from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import check_figures_equal def draw_quiver(ax, **kwargs): @@ -333,3 +334,53 @@ def test_quiver_setuvc_numbers(): q = ax.quiver(X, Y, U, V) q.set_UVC(0, 1) + + +def draw_quiverkey_zorder_argument(fig, zorder=None): + """Draw Quiver and QuiverKey using zorder argument""" + x = np.arange(1, 6, 1) + y = np.arange(1, 6, 1) + X, Y = np.meshgrid(x, y) + U, V = 2, 2 + + ax = fig.subplots() + q = ax.quiver(X, Y, U, V, pivot='middle') + ax.set_xlim(0.5, 5.5) + ax.set_ylim(0.5, 5.5) + if zorder is None: + ax.quiverkey(q, 4, 4, 25, coordinates='data', + label='U', color='blue') + ax.quiverkey(q, 5.5, 2, 20, coordinates='data', + label='V', color='blue', angle=90) + else: + ax.quiverkey(q, 4, 4, 25, coordinates='data', + label='U', color='blue', zorder=zorder) + ax.quiverkey(q, 5.5, 2, 20, coordinates='data', + label='V', color='blue', angle=90, zorder=zorder) + + +def draw_quiverkey_setzorder(fig, zorder=None): + """Draw Quiver and QuiverKey using set_zorder""" + x = np.arange(1, 6, 1) + y = np.arange(1, 6, 1) + X, Y = np.meshgrid(x, y) + U, V = 2, 2 + + ax = fig.subplots() + q = ax.quiver(X, Y, U, V, pivot='middle') + ax.set_xlim(0.5, 5.5) + ax.set_ylim(0.5, 5.5) + qk1 = ax.quiverkey(q, 4, 4, 25, coordinates='data', + label='U', color='blue') + qk2 = ax.quiverkey(q, 5.5, 2, 20, coordinates='data', + label='V', color='blue', angle=90) + if zorder is not None: + qk1.set_zorder(zorder) + qk2.set_zorder(zorder) + + +@pytest.mark.parametrize('zorder', [0, 2, 5, None]) +@check_figures_equal(extensions=['png']) +def test_quiverkey_zorder(fig_test, fig_ref, zorder): + draw_quiverkey_zorder_argument(fig_test, zorder=zorder) + draw_quiverkey_setzorder(fig_ref, zorder=zorder) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 0aa3ec0ba603..13633956c349 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -554,6 +554,7 @@ def test_backend_fallback_headful(tmp_path): # Check that access on another instance does not resolve the sentinel. "assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; " "assert mpl.rcParams._get('backend') == sentinel; " + "assert mpl.get_backend(auto_select=False) is None; " "import matplotlib.pyplot; " "print(matplotlib.get_backend())"], env=env, text=True, check=True, capture_output=True).stdout diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py index 0a5c215eff30..a052c24cb655 100644 --- a/lib/matplotlib/tests/test_simplification.py +++ b/lib/matplotlib/tests/test_simplification.py @@ -518,3 +518,54 @@ def test_clipping_full(): simplified = list(p.iter_segments(clip=[0, 0, 100, 100])) assert ([(list(x), y) for x, y in simplified] == [([50, 40], 1)]) + + +def test_simplify_closepoly(): + # The values of the vertices in a CLOSEPOLY should always be ignored, + # in favor of the most recent MOVETO's vertex values + paths = [Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan)], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]), + Path([(1, 1), (2, 1), (2, 2), (40, 50)], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])] + expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1), (1, 1), (0, 0)], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.LINETO, Path.STOP]) + + for path in paths: + simplified_path = path.cleaned(simplify=True) + assert_array_equal(expected_path.vertices, simplified_path.vertices) + assert_array_equal(expected_path.codes, simplified_path.codes) + + # test that a compound path also works + path = Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan), + (-1, 0), (-2, 0), (-2, 1), (np.nan, np.nan)], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]) + expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1), + (-1, 0), (-2, 0), (-2, 1), (-1, 0), (-1, 0), (0, 0)], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.LINETO, Path.STOP]) + + simplified_path = path.cleaned(simplify=True) + assert_array_equal(expected_path.vertices, simplified_path.vertices) + assert_array_equal(expected_path.codes, simplified_path.codes) + + # test for a path with an invalid MOVETO + # CLOSEPOLY with an invalid MOVETO should be ignored + path = Path([(1, 0), (1, -1), (2, -1), + (np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1), + (2, 2), (0, -1)], + [Path.MOVETO, Path.LINETO, Path.LINETO, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.CLOSEPOLY, Path.LINETO]) + expected_path = Path([(1, 0), (1, -1), (2, -1), + (np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1), + (0, -1), (0, -1), (0, 0)], + [Path.MOVETO, Path.LINETO, Path.LINETO, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.LINETO, Path.LINETO, Path.STOP]) + + simplified_path = path.cleaned(simplify=True) + assert_array_equal(expected_path.vertices, simplified_path.vertices) + assert_array_equal(expected_path.codes, simplified_path.codes) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 6624e3b17ba5..6e7b5ec5e50e 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -10,8 +10,7 @@ import pytest -pytest.importorskip('sphinx', - minversion=None if sys.version_info < (3, 10) else '4.1.3') +pytest.importorskip('sphinx', minversion='4.1.3') def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): @@ -63,7 +62,7 @@ def plot_directive_file(num): # This is always next to the doctree dir. return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png' - range_10, range_6, range_4 = [plot_file(i) for i in range(1, 4)] + range_10, range_6, range_4 = (plot_file(i) for i in range(1, 4)) # Plot 5 is range(6) plot assert filecmp.cmp(range_6, plot_file(5)) # Plot 7 is range(4) plot diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index ea31ac124e4a..ee974f3cd8f9 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -2,10 +2,8 @@ from unittest.mock import Mock import numpy as np -import pytest import matplotlib.pyplot as plt -import matplotlib as mpl from matplotlib.path import Path from matplotlib.table import CustomCell, Table from matplotlib.testing.decorators import image_comparison, check_figures_equal @@ -128,10 +126,9 @@ def test_customcell(): @image_comparison(['table_auto_column.png']) def test_auto_column(): - fig = plt.figure() + fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1) # iterable list input - ax1 = fig.add_subplot(4, 1, 1) ax1.axis('off') tb1 = ax1.table( cellText=[['Fit Text', 2], @@ -144,7 +141,6 @@ def test_auto_column(): tb1.auto_set_column_width([-1, 0, 1]) # iterable tuple input - ax2 = fig.add_subplot(4, 1, 2) ax2.axis('off') tb2 = ax2.table( cellText=[['Fit Text', 2], @@ -157,7 +153,6 @@ def test_auto_column(): tb2.auto_set_column_width((-1, 0, 1)) # 3 single inputs - ax3 = fig.add_subplot(4, 1, 3) ax3.axis('off') tb3 = ax3.table( cellText=[['Fit Text', 2], @@ -171,8 +166,8 @@ def test_auto_column(): tb3.auto_set_column_width(0) tb3.auto_set_column_width(1) - # 4 non integer iterable input - ax4 = fig.add_subplot(4, 1, 4) + # 4 this used to test non-integer iterable input, which did nothing, but only + # remains to avoid re-generating the test image. ax4.axis('off') tb4 = ax4.table( cellText=[['Fit Text', 2], @@ -182,12 +177,6 @@ def test_auto_column(): loc="center") tb4.auto_set_font_size(False) tb4.set_fontsize(12) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width("-101") # type: ignore [arg-type] - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width(["-101"]) # type: ignore [list-item] def test_table_cells(): @@ -264,3 +253,20 @@ def __repr__(self): munits.registry.pop(FakeUnit) assert not munits.registry.get_converter(FakeUnit) + + +def test_table_dataframe(pd): + # Test if Pandas Data Frame can be passed in cellText + + data = { + 'Letter': ['A', 'B', 'C'], + 'Number': [100, 200, 300] + } + + df = pd.DataFrame(data) + fig, ax = plt.subplots() + table = ax.table(df, loc='center') + + for r, (index, row) in enumerate(df.iterrows()): + for c, col in enumerate(df.columns if r == 0 else row.values): + assert table[r if r == 0 else r+1, c].get_text().get_text() == str(col) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 8904337f68ba..19262202e5c1 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -921,7 +921,7 @@ def test_annotate_offset_fontsize(): fontsize='10', xycoords='data', textcoords=text_coords[i]) for i in range(2)] - points_coords, fontsize_coords = [ann.get_window_extent() for ann in anns] + points_coords, fontsize_coords = (ann.get_window_extent() for ann in anns) fig.canvas.draw() assert str(points_coords) == str(fontsize_coords) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index ac68a5d90b14..77c0e917df8a 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -362,15 +362,12 @@ def test_switch_to_autolocator(self): def test_set_params(self): """ Create log locator with default value, base=10.0, subs=[1.0], - numdecs=4, numticks=15 and change it to something else. + numticks=15 and change it to something else. See if change was successful. Should not raise exception. """ loc = mticker.LogLocator() - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="numdecs"): - loc.set_params(numticks=7, numdecs=8, subs=[2.0], base=4) + loc.set_params(numticks=7, subs=[2.0], base=4) assert loc.numticks == 7 - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="numdecs"): - assert loc.numdecs == 8 assert loc._base == 4 assert list(loc._subs) == [2.0] @@ -637,7 +634,7 @@ def test_subs(self): sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, subs=[2.0, 4.0]) sym.create_dummy_axis() sym.axis.set_view_interval(-10, 10) - assert (sym() == [-20., -40., -2., -4., 0., 2., 4., 20., 40.]).all() + assert_array_equal(sym(), [-20, -40, -2, -4, 0, 2, 4, 20, 40]) def test_extending(self): sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) @@ -1594,6 +1591,73 @@ def test_engformatter_usetex_useMathText(): assert x_tick_label_text == ['$0$', '$500$', '$1$ k'] +@pytest.mark.parametrize( + 'data_offset, noise, oom_center_desired, oom_noise_desired', [ + (271_490_000_000.0, 10, 9, 0), + (27_149_000_000_000.0, 10_000_000, 12, 6), + (27.149, 0.01, 0, -3), + (2_714.9, 0.01, 3, -3), + (271_490.0, 0.001, 3, -3), + (271.49, 0.001, 0, -3), + # The following sets of parameters demonstrates that when + # oom(data_offset)-1 and oom(noise)-2 equal a standard 3*N oom, we get + # that oom_noise_desired < oom(noise) + (27_149_000_000.0, 100, 9, +3), + (27.149, 1e-07, 0, -6), + (271.49, 0.0001, 0, -3), + (27.149, 0.0001, 0, -3), + # Tests where oom(data_offset) <= oom(noise), those are probably + # covered by the part where formatter.offset != 0 + (27_149.0, 10_000, 0, 3), + (27.149, 10_000, 0, 3), + (27.149, 1_000, 0, 3), + (27.149, 100, 0, 0), + (27.149, 10, 0, 0), + ] +) +def test_engformatter_offset_oom( + data_offset, + noise, + oom_center_desired, + oom_noise_desired +): + UNIT = "eV" + fig, ax = plt.subplots() + ydata = data_offset + np.arange(-5, 7, dtype=float)*noise + ax.plot(ydata) + formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) + # So that offset strings will always have the same size + formatter.ENG_PREFIXES[0] = "_" + ax.yaxis.set_major_formatter(formatter) + fig.canvas.draw() + offset_got = formatter.get_offset() + ticks_got = [labl.get_text() for labl in ax.get_yticklabels()] + # Predicting whether offset should be 0 or not is essentially testing + # ScalarFormatter._compute_offset . This function is pretty complex and it + # would be nice to test it, but this is out of scope for this test which + # only makes sure that offset text and the ticks gets the correct unit + # prefixes and the ticks. + if formatter.offset: + prefix_noise_got = offset_got[2] + prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] + prefix_center_got = offset_got[-1-len(UNIT)] + prefix_center_desired = formatter.ENG_PREFIXES[oom_center_desired] + assert prefix_noise_desired == prefix_noise_got + assert prefix_center_desired == prefix_center_got + # Make sure the ticks didn't get the UNIT + for tick in ticks_got: + assert UNIT not in tick + else: + assert oom_center_desired == 0 + assert offset_got == "" + # Make sure the ticks contain now the prefixes + for tick in ticks_got: + # 0 is zero on all orders of magnitudes, no matter what is + # oom_noise_desired + prefix_idx = 0 if tick[0] == "0" else oom_noise_desired + assert tick.endswith(formatter.ENG_PREFIXES[prefix_idx] + UNIT) + + class TestPercentFormatter: percent_data = [ # Check explicitly set decimals over different intervals and values diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 3d12b90d5210..96e78b6828f8 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -9,9 +9,10 @@ import matplotlib.pyplot as plt import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms -from matplotlib.transforms import Affine2D, Bbox, TransformedBbox +from matplotlib.transforms import Affine2D, Bbox, TransformedBbox, _ScaledRotation from matplotlib.path import Path from matplotlib.testing.decorators import image_comparison, check_figures_equal +from unittest.mock import MagicMock class TestAffine2D: @@ -341,6 +342,31 @@ def test_deepcopy(self): assert_array_equal(s.get_matrix(), a.get_matrix()) +class TestAffineDeltaTransform: + def test_invalidate(self): + before = np.array([[1.0, 4.0, 0.0], + [5.0, 1.0, 0.0], + [0.0, 0.0, 1.0]]) + after = np.array([[1.0, 3.0, 0.0], + [5.0, 1.0, 0.0], + [0.0, 0.0, 1.0]]) + + # Translation and skew present + base = mtransforms.Affine2D.from_values(1, 5, 4, 1, 2, 3) + t = mtransforms.AffineDeltaTransform(base) + assert_array_equal(t.get_matrix(), before) + + # Mess with the internal structure of `base` without invalidating + # This should not affect this transform because it's a passthrough: + # it's always invalid + base.get_matrix()[0, 1:] = 3 + assert_array_equal(t.get_matrix(), after) + + # Invalidate the base + base.invalidate() + assert_array_equal(t.get_matrix(), after) + + def test_non_affine_caching(): class AssertingNonAffineTransform(mtransforms.Transform): """ @@ -1079,3 +1105,27 @@ def test_interval_contains_open(): 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(): + """Test that the ScaledRotation object is initialized correctly.""" + theta = 1.0 # Arbitrary theta value for testing + trans_shift = MagicMock() # Mock the trans_shift transformation + scaled_rot = _ScaledRotation(theta, trans_shift) + assert scaled_rot._theta == theta + assert scaled_rot._trans_shift == trans_shift + assert scaled_rot._mtx is None + + +def test_scaledrotation_get_matrix_invalid(): + """Test get_matrix when the matrix is invalid and needs recalculation.""" + theta = np.pi / 2 + trans_shift = MagicMock(transform=MagicMock(return_value=[[theta, 0]])) + scaled_rot = _ScaledRotation(theta, trans_shift) + scaled_rot._invalid = True + matrix = scaled_rot.get_matrix() + trans_shift.transform.assert_called_once_with([[theta, 0]]) + expected_rotation = np.array([[0, -1], + [1, 0]]) + assert matrix is not None + assert_allclose(matrix[:2, :2], expected_rotation, atol=1e-15) diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index 14c591abd4e5..337443eb1e27 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -1181,43 +1181,44 @@ def test_tricontourf_decreasing_levels(): plt.tricontourf(x, y, z, [1.0, 0.0]) -def test_internal_cpp_api(): +def test_internal_cpp_api() -> None: # Following github issue 8197. - from matplotlib import _tri # noqa: ensure lazy-loaded module *is* loaded. + from matplotlib import _tri # noqa: F401, ensure lazy-loaded module *is* loaded. # C++ Triangulation. with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.Triangulation() + mpl._tri.Triangulation() # type: ignore[call-arg] with pytest.raises( ValueError, match=r'x and y must be 1D arrays of the same length'): - mpl._tri.Triangulation([], [1], [[]], (), (), (), False) + mpl._tri.Triangulation(np.array([]), np.array([1]), np.array([[]]), (), (), (), + False) - x = [0, 1, 1] - y = [0, 0, 1] + x = np.array([0, 1, 1], dtype=np.float64) + y = np.array([0, 0, 1], dtype=np.float64) with pytest.raises( ValueError, match=r'triangles must be a 2D array of shape \(\?,3\)'): - mpl._tri.Triangulation(x, y, [[0, 1]], (), (), (), False) + mpl._tri.Triangulation(x, y, np.array([[0, 1]]), (), (), (), False) - tris = [[0, 1, 2]] + tris = np.array([[0, 1, 2]], dtype=np.int_) with pytest.raises( ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, [0, 1], (), (), False) + mpl._tri.Triangulation(x, y, tris, np.array([0, 1]), (), (), False) with pytest.raises( ValueError, match=r'edges must be a 2D array with shape \(\?,2\)'): - mpl._tri.Triangulation(x, y, tris, (), [[1]], (), False) + mpl._tri.Triangulation(x, y, tris, (), np.array([[1]]), (), False) with pytest.raises( ValueError, match=r'neighbors must be a 2D array with the same shape as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, (), (), [[-1]], False) + mpl._tri.Triangulation(x, y, tris, (), (), np.array([[-1]]), False) triang = mpl._tri.Triangulation(x, y, tris, (), (), (), False) @@ -1232,9 +1233,9 @@ def test_internal_cpp_api(): ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - triang.set_mask(mask) + triang.set_mask(mask) # type: ignore[arg-type] - triang.set_mask([True]) + triang.set_mask(np.array([True])) assert_array_equal(triang.get_edges(), np.empty((0, 2))) triang.set_mask(()) # Equivalent to Python Triangulation mask=None @@ -1244,15 +1245,14 @@ def test_internal_cpp_api(): with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.TriContourGenerator() + mpl._tri.TriContourGenerator() # type: ignore[call-arg] with pytest.raises( ValueError, - match=r'z must be a 1D array with the same length as the x and y ' - r'arrays'): - mpl._tri.TriContourGenerator(triang, [1]) + match=r'z must be a 1D array with the same length as the x and y arrays'): + mpl._tri.TriContourGenerator(triang, np.array([1])) - z = [0, 1, 2] + z = np.array([0, 1, 2]) tcg = mpl._tri.TriContourGenerator(triang, z) with pytest.raises( @@ -1263,13 +1263,13 @@ def test_internal_cpp_api(): with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.TrapezoidMapTriFinder() + mpl._tri.TrapezoidMapTriFinder() # type: ignore[call-arg] trifinder = mpl._tri.TrapezoidMapTriFinder(triang) with pytest.raises( ValueError, match=r'x and y must be array-like with same shape'): - trifinder.find_many([0], [0, 1]) + trifinder.find_many(np.array([0]), np.array([0, 1])) def test_qhull_large_offset(): diff --git a/lib/matplotlib/tests/test_ttconv.py b/lib/matplotlib/tests/test_ttconv.py deleted file mode 100644 index 1d839e7094b0..000000000000 --- a/lib/matplotlib/tests/test_ttconv.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path - -import matplotlib -from matplotlib.testing.decorators import image_comparison -import matplotlib.pyplot as plt - - -@image_comparison(["truetype-conversion.pdf"]) -# mpltest.ttf does not have "l"/"p" glyphs so we get a warning when trying to -# get the font extents. -def test_truetype_conversion(recwarn): - matplotlib.rcParams['pdf.fonttype'] = 3 - fig, ax = plt.subplots() - ax.text(0, 0, "ABCDE", - font=Path(__file__).with_name("mpltest.ttf"), fontsize=80) - ax.set_xticks([]) - ax.set_yticks([]) diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index 1e173d5ea84d..9b8a2d1f07c6 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -141,11 +141,11 @@ def test_overprecision(): font = t1f.Type1Font(filename) slanted = font.transform({'slant': .167}) lines = slanted.parts[0].decode('ascii').splitlines() - matrix, = [line[line.index('[')+1:line.index(']')] - for line in lines if '/FontMatrix' in line] - angle, = [word + matrix, = (line[line.index('[')+1:line.index(']')] + for line in lines if '/FontMatrix' in line) + angle, = (word for line in lines if '/ItalicAngle' in line - for word in line.split() if word[0] in '-0123456789'] + for word in line.split() if word[0] in '-0123456789') # the following used to include 0.00016700000000000002 assert matrix == '0.001 0 0.000167 0.001 0 0' # and here we had -9.48090361795083 diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index ae6372fea1e1..cc71f685857e 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -4,8 +4,10 @@ import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal, image_comparison +import matplotlib.patches as mpatches import matplotlib.units as munits -from matplotlib.category import UnitData +from matplotlib.category import StrCategoryConverter, UnitData +from matplotlib.dates import DateConverter import numpy as np import pytest @@ -236,6 +238,39 @@ def test_shared_axis_categorical(): assert "c" in ax2.xaxis.get_units()._mapping.keys() +def test_explicit_converter(): + d1 = {"a": 1, "b": 2} + str_cat_converter = StrCategoryConverter() + str_cat_converter_2 = StrCategoryConverter() + date_converter = DateConverter() + + # Explicit is set + fig1, ax1 = plt.subplots() + ax1.xaxis.set_converter(str_cat_converter) + assert ax1.xaxis.get_converter() == str_cat_converter + # Explicit not overridden by implicit + ax1.plot(d1.keys(), d1.values()) + assert ax1.xaxis.get_converter() == str_cat_converter + # No error when called twice with equivalent input + ax1.xaxis.set_converter(str_cat_converter) + # Error when explicit called twice + with pytest.raises(RuntimeError): + ax1.xaxis.set_converter(str_cat_converter_2) + + fig2, ax2 = plt.subplots() + ax2.plot(d1.keys(), d1.values()) + + # No error when equivalent type is used + ax2.xaxis.set_converter(str_cat_converter) + + fig3, ax3 = plt.subplots() + ax3.plot(d1.keys(), d1.values()) + + # Warn when implicit overridden + with pytest.warns(): + ax3.xaxis.set_converter(date_converter) + + def test_empty_default_limits(quantity_converter): munits.registry[Quantity] = quantity_converter fig, ax1 = plt.subplots() @@ -302,3 +337,17 @@ def test_plot_kernel(): # just a smoketest that fail kernel = Kernel([1, 2, 3, 4, 5]) plt.plot(kernel) + + +def test_connection_patch_units(pd): + # tests that this doesn't raise an error + fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 5)) + x = pd.Timestamp('2017-01-01T12') + ax1.axvline(x) + y = "test test" + ax2.axhline(y) + arr = mpatches.ConnectionPatch((x, 0), (0, y), + coordsA='data', coordsB='data', + axesA=ax1, axesB=ax2) + fig.add_artist(arr) + fig.draw_without_rendering() diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0f2cc411dbdf..585d846944e8 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1,8 +1,8 @@ import functools import io +import operator from unittest import mock -import matplotlib as mpl from matplotlib.backend_bases import MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets @@ -70,7 +70,7 @@ def test_save_blitted_widget_as_pdf(): def test_rectangle_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, **kwargs) + tool = widgets.RectangleSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=199, ydata=199, button=1) @@ -104,7 +104,7 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): minspanx, minspany = (ax.transData.transform((x1, y1)) - ax.transData.transform((x0, y0))) - tool = widgets.RectangleSelector(ax, onselect, interactive=True, + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=True, spancoords=spancoords, minspanx=minspanx, minspany=minspany) # Too small to create a selector @@ -130,21 +130,11 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): assert kwargs == {} -def test_deprecation_selector_visible_attribute(ax): - tool = widgets.RectangleSelector(ax, lambda *args: None) - - assert tool.get_visible() - - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="was deprecated in Matplotlib 3.8"): - tool.visible - - @pytest.mark.parametrize('drag_from_anywhere, new_center', [[True, (60, 75)], [False, (30, 20)]]) def test_rectangle_drag(ax, drag_from_anywhere, new_center): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, drag_from_anywhere=drag_from_anywhere) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) @@ -165,7 +155,7 @@ def test_rectangle_drag(ax, drag_from_anywhere, new_center): def test_rectangle_selector_set_props_handle_props(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, props=dict(facecolor='b', alpha=0.2), handle_props=dict(alpha=0.5)) # Create rectangle @@ -186,7 +176,7 @@ def test_rectangle_selector_set_props_handle_props(ax): def test_rectangle_resize(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) assert tool.extents == (0.0, 100.0, 10.0, 120.0) @@ -221,7 +211,7 @@ def test_rectangle_resize(ax): def test_rectangle_add_state(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) @@ -237,7 +227,7 @@ def test_rectangle_add_state(ax): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_center(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) assert tool.extents == (70.0, 125.0, 65.0, 130.0) @@ -311,7 +301,7 @@ def test_rectangle_resize_center(ax, add_state): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_square(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) assert tool.extents == (70.0, 120.0, 65.0, 115.0) @@ -384,7 +374,7 @@ def test_rectangle_resize_square(ax, add_state): def test_rectangle_resize_square_center(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) tool.add_state('square') @@ -449,7 +439,7 @@ def test_rectangle_resize_square_center(ax): @pytest.mark.parametrize('selector_class', [widgets.RectangleSelector, widgets.EllipseSelector]) def test_rectangle_rotate(ax, selector_class): - tool = selector_class(ax, onselect=noop, interactive=True) + tool = selector_class(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -482,7 +472,7 @@ def test_rectangle_rotate(ax, selector_class): def test_rectangle_add_remove_set(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -498,7 +488,7 @@ def test_rectangle_add_remove_set(ax): def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): ax.set_aspect(0.8) - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, use_data_coordinates=use_data_coordinates) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) @@ -530,8 +520,7 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): def test_ellipse(ax): """For ellipse, test out the key modifiers""" - tool = widgets.EllipseSelector(ax, onselect=noop, - grab_range=10, interactive=True) + tool = widgets.EllipseSelector(ax, grab_range=10, interactive=True) tool.extents = (100, 150, 100, 150) # drag the rectangle @@ -557,9 +546,7 @@ def test_ellipse(ax): def test_rectangle_handles(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, - grab_range=10, - interactive=True, + tool = widgets.RectangleSelector(ax, grab_range=10, interactive=True, handle_props={'markerfacecolor': 'r', 'markeredgecolor': 'b'}) tool.extents = (100, 150, 100, 150) @@ -594,7 +581,7 @@ def test_rectangle_selector_onselect(ax, interactive): # check when press and release events take place at the same position onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, interactive=interactive) + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=interactive) # move outside of axis click_and_drag(tool, start=(100, 110), end=(150, 120)) @@ -610,7 +597,7 @@ def test_rectangle_selector_onselect(ax, interactive): def test_rectangle_selector_ignore_outside(ax, ignore_event_outside): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, + tool = widgets.RectangleSelector(ax, onselect=onselect, ignore_event_outside=ignore_event_outside) click_and_drag(tool, start=(100, 110), end=(150, 120)) onselect.assert_called_once() @@ -772,10 +759,11 @@ def test_span_selector_set_props_handle_props(ax): @pytest.mark.parametrize('selector', ['span', 'rectangle']) def test_selector_clear(ax, selector): - kwargs = dict(ax=ax, onselect=noop, interactive=True) + kwargs = dict(ax=ax, interactive=True) if selector == 'span': Selector = widgets.SpanSelector kwargs['direction'] = 'horizontal' + kwargs['onselect'] = noop else: Selector = widgets.RectangleSelector @@ -806,7 +794,7 @@ def test_selector_clear_method(ax, selector): interactive=True, ignore_event_outside=True) else: - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) click_and_drag(tool, start=(10, 10), end=(100, 120)) assert tool._selection_completed assert tool.get_visible() @@ -862,7 +850,7 @@ def test_tool_line_handle(ax): def test_span_selector_bound(direction): fig, ax = plt.subplots(1, 1) ax.plot([10, 20], [10, 30]) - ax.figure.canvas.draw() + fig.canvas.draw() x_bound = ax.get_xbound() y_bound = ax.get_ybound() @@ -999,7 +987,7 @@ def test_span_selector_extents(ax): def test_lasso_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, **kwargs) + tool = widgets.LassoSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=125, ydata=125, button=1) do_event(tool, 'release', xdata=150, ydata=150, button=1) @@ -1010,7 +998,8 @@ def test_lasso_selector(ax, kwargs): def test_lasso_selector_set_props(ax): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, props=dict(color='b', alpha=0.2)) + tool = widgets.LassoSelector(ax, onselect=onselect, + props=dict(color='b', alpha=0.2)) artist = tool._selection_artist assert mcolors.same_color(artist.get_color(), 'b') @@ -1109,7 +1098,7 @@ def test_RadioButtons(ax): @image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True) def test_check_radio_buttons_image(): ax = get_ax() - fig = ax.figure + fig = ax.get_figure(root=False) fig.subplots_adjust(left=0.3) rax1 = fig.add_axes((0.05, 0.7, 0.2, 0.15)) @@ -1379,7 +1368,7 @@ def check_polygon_selector(event_sequence, expected_result, selections_count, onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.PolygonSelector(ax, onselect, **kwargs) + tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) @@ -1516,7 +1505,7 @@ def test_polygon_selector(draw_bounding_box): @pytest.mark.parametrize('draw_bounding_box', [False, True]) def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box): - tool = widgets.PolygonSelector(ax, onselect=noop, + tool = widgets.PolygonSelector(ax, props=dict(color='b', alpha=0.2), handle_props=dict(alpha=0.5), draw_bounding_box=draw_bounding_box) @@ -1553,8 +1542,7 @@ def test_rect_visibility(fig_test, fig_ref): ax_test = fig_test.subplots() _ = fig_ref.subplots() - tool = widgets.RectangleSelector(ax_test, onselect=noop, - props={'visible': False}) + tool = widgets.RectangleSelector(ax_test, props={'visible': False}) tool.extents = (0.2, 0.8, 0.3, 0.7) @@ -1573,7 +1561,7 @@ def test_polygon_selector_remove(idx, draw_bounding_box): # Remove the extra point event_sequence.append(polygon_remove_vertex(200, 200)) # Flatten list of lists - event_sequence = sum(event_sequence, []) + event_sequence = functools.reduce(operator.iadd, event_sequence, []) check_polygon_selector(event_sequence, verts, 2, draw_bounding_box=draw_bounding_box) @@ -1607,8 +1595,7 @@ def test_polygon_selector_redraw(ax, draw_bounding_box): *polygon_place_vertex(*verts[1]), ] - tool = widgets.PolygonSelector(ax, onselect=noop, - draw_bounding_box=draw_bounding_box) + tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) # After removing two verts, only one remains, and the @@ -1622,14 +1609,12 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)] ax_test = fig_test.add_subplot() - tool_test = widgets.PolygonSelector( - ax_test, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_test = widgets.PolygonSelector(ax_test, draw_bounding_box=draw_bounding_box) tool_test.verts = verts assert tool_test.verts == verts ax_ref = fig_ref.add_subplot() - tool_ref = widgets.PolygonSelector( - ax_ref, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box) event_sequence = [ *polygon_place_vertex(*verts[0]), *polygon_place_vertex(*verts[1]), @@ -1653,14 +1638,14 @@ def test_polygon_selector_box(ax): ] # Create selector - tool = widgets.PolygonSelector(ax, onselect=noop, draw_bounding_box=True) + tool = widgets.PolygonSelector(ax, draw_bounding_box=True) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) # In order to trigger the correct callbacks, trigger events on the canvas # instead of the individual tools t = ax.transData - canvas = ax.figure.canvas + canvas = ax.get_figure(root=True).canvas # Scale to half size using the top right corner of the bounding box MouseEvent( @@ -1722,7 +1707,8 @@ def test_polygon_selector_clear_method(ax): @pytest.mark.parametrize("horizOn", [False, True]) @pytest.mark.parametrize("vertOn", [False, True]) def test_MultiCursor(horizOn, vertOn): - (ax1, ax3) = plt.figure().subplots(2, sharex=True) + 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 @@ -1740,7 +1726,7 @@ def test_MultiCursor(horizOn, vertOn): event = mock_event(ax1, xdata=.5, ydata=.25) multi.onmove(event) # force a draw + draw event to exercise clear - ax1.figure.canvas.draw() + fig.canvas.draw() # the lines in the first two ax should both move for l in multi.vlines: diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 812eab58b877..a374bfba8cab 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -31,7 +31,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, dviread +from matplotlib import cbook, dviread _log = logging.getLogger(__name__) @@ -63,7 +63,6 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - texcache = _api.deprecate_privatize_attribute("3.8") _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} @@ -134,18 +133,16 @@ def _get_font_preamble_and_command(cls): preambles[font_family] = cls._font_preambles[ mpl.rcParams['font.family'][0].lower()] else: - for font in mpl.rcParams['font.' + font_family]: - if font.lower() in cls._font_preambles: - preambles[font_family] = \ - cls._font_preambles[font.lower()] + rcfonts = mpl.rcParams[f"font.{font_family}"] + for i, font in enumerate(map(str.lower, rcfonts)): + if font in cls._font_preambles: + preambles[font_family] = cls._font_preambles[font] _log.debug( - 'family: %s, font: %s, info: %s', - font_family, font, - cls._font_preambles[font.lower()]) + 'family: %s, package: %s, font: %s, skipped: %s', + font_family, cls._font_preambles[font], rcfonts[i], + ', '.join(rcfonts[:i]), + ) break - else: - _log.debug('%s font is not compatible with usetex.', - font) else: _log.info('No LaTeX-compatible font found for the %s font' 'family in rcParams. Using default.', diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index af990ec1bf9f..0b65450f760b 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -372,7 +372,8 @@ def _get_layout(self, renderer): # Full vertical extent of font, including ascenders and descenders: _, lp_h, lp_d = _get_text_metrics_with_cache( renderer, "lp", self._fontproperties, - ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi) + ismath="TeX" if self.get_usetex() else False, + dpi=self.get_figure(root=True).dpi) min_dy = (lp_h - lp_d) * self._linespacing for i, line in enumerate(lines): @@ -380,7 +381,7 @@ def _get_layout(self, renderer): if clean_line: w, h, d = _get_text_metrics_with_cache( renderer, clean_line, self._fontproperties, - ismath=ismath, dpi=self.figure.dpi) + ismath=ismath, dpi=self.get_figure(root=True).dpi) else: w = h = d = 0 @@ -753,9 +754,16 @@ def draw(self, renderer): # don't use self.get_position here, which refers to text # position in Text: - posx = float(self.convert_xunits(self._x)) - posy = float(self.convert_yunits(self._y)) + x, y = self._x, self._y + if np.ma.is_masked(x): + x = np.nan + if np.ma.is_masked(y): + y = np.nan + posx = float(self.convert_xunits(x)) + posy = float(self.convert_yunits(y)) posx, posy = trans.transform((posx, posy)) + if np.isnan(posx) or np.isnan(posy): + return # don't throw a warning here if not np.isfinite(posx) or not np.isfinite(posy): _log.warning("posx and posy should be finite values") return @@ -934,28 +942,30 @@ def get_window_extent(self, renderer=None, dpi=None): dpi : float, optional The dpi value for computing the bbox, defaults to - ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if - to match regions with a figure saved with a custom dpi value. + ``self.get_figure(root=True).dpi`` (*not* the renderer dpi); should be set + e.g. if to match regions with a figure saved with a custom dpi value. """ if not self.get_visible(): return Bbox.unit() + + fig = self.get_figure(root=True) if dpi is None: - dpi = self.figure.dpi + dpi = fig.dpi if self.get_text() == '': - with cbook._setattr_cm(self.figure, dpi=dpi): + with cbook._setattr_cm(fig, dpi=dpi): tx, ty = self._get_xy_display() return Bbox.from_bounds(tx, ty, 0, 0) if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._get_renderer() + self._renderer = fig._get_renderer() if self._renderer is None: raise RuntimeError( "Cannot get window extent of text w/o renderer. You likely " "want to call 'figure.draw_without_rendering()' first.") - with cbook._setattr_cm(self.figure, dpi=dpi): + with cbook._setattr_cm(fig, dpi=dpi): bbox, info, descent = self._get_layout(self._renderer) x, y = self.get_unitless_position() x, y = self.get_transform().transform((x, y)) @@ -1514,9 +1524,9 @@ def _get_xy_transform(self, renderer, coords): # if unit is offset-like if bbox_name == "figure": - bbox0 = self.figure.figbbox + bbox0 = self.get_figure(root=False).figbbox elif bbox_name == "subfigure": - bbox0 = self.figure.bbox + bbox0 = self.get_figure(root=False).bbox elif bbox_name == "axes": bbox0 = self.axes.bbox @@ -1529,11 +1539,13 @@ def _get_xy_transform(self, renderer, coords): raise ValueError(f"{coords!r} is not a valid coordinate") if unit == "points": - tr = Affine2D().scale(self.figure.dpi / 72) # dpi/72 dots per point + tr = Affine2D().scale( + self.get_figure(root=True).dpi / 72) # dpi/72 dots per point elif unit == "pixels": tr = Affine2D() elif unit == "fontsize": - tr = Affine2D().scale(self.get_size() * self.figure.dpi / 72) + tr = Affine2D().scale( + self.get_size() * self.get_figure(root=True).dpi / 72) elif unit == "fraction": tr = Affine2D().scale(*bbox0.size) else: @@ -1571,7 +1583,7 @@ def _get_position_xy(self, renderer): def _check_xy(self, renderer=None): """Check whether the annotation at *xy_pixel* should be drawn.""" if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() b = self.get_annotation_clip() if b or (b is None and self.xycoords == "data"): # check if self.xy is inside the Axes. @@ -1837,10 +1849,6 @@ def transform(renderer) -> Transform # modified YAArrow API to be used with FancyArrowPatch for key in ['width', 'headwidth', 'headlength', 'shrink']: arrowprops.pop(key, None) - if 'frac' in arrowprops: - _api.warn_deprecated( - "3.8", name="the (unused) 'frac' key in 'arrowprops'") - arrowprops.pop("frac") self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops) else: self.arrow_patch = None @@ -1848,7 +1856,6 @@ def transform(renderer) -> Transform # Must come last, as some kwargs may be propagated to arrow_patch. Text.__init__(self, x, y, text, **kwargs) - @_api.rename_parameter("3.8", "event", "mouseevent") def contains(self, mouseevent): if self._different_canvas(mouseevent): return False, {} @@ -1987,8 +1994,9 @@ def draw(self, renderer): self.update_positions(renderer) self.update_bbox_position_size(renderer) if self.arrow_patch is not None: # FancyArrowPatch - if self.arrow_patch.figure is None and self.figure is not None: - self.arrow_patch.figure = self.figure + if (self.arrow_patch.get_figure(root=False) is None and + (fig := self.get_figure(root=False)) is not None): + self.arrow_patch.set_figure(fig) self.arrow_patch.draw(renderer) # Draw text, including FancyBboxPatch, after FancyArrowPatch. # Otherwise, a wedge arrowstyle can land partly on top of the Bbox. @@ -2003,7 +2011,7 @@ def get_window_extent(self, renderer=None): if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._get_renderer() + self._renderer = self.get_figure(root=True)._get_renderer() if self._renderer is None: raise RuntimeError('Cannot get window extent without renderer') @@ -2024,4 +2032,4 @@ def get_tightbbox(self, renderer=None): return super().get_tightbbox(renderer) -_docstring.interpd.update(Annotation=Annotation.__init__.__doc__) +_docstring.interpd.register(Annotation=Annotation.__init__.__doc__) diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 6a83b1bbbed9..d65a3dc4c7da 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -4,7 +4,7 @@ from .font_manager import FontProperties from .offsetbox import DraggableAnnotation from .path import Path from .patches import FancyArrowPatch, FancyBboxPatch -from .textpath import ( # noqa: reexported API +from .textpath import ( # noqa: F401, reexported API TextPath as TextPath, TextToPath as TextToPath, ) @@ -16,7 +16,7 @@ from .transforms import ( from collections.abc import Callable, Iterable from typing import Any, Literal -from .typing import ColorType +from .typing import ColorType, CoordsType class Text(Artist): zorder: float @@ -120,17 +120,11 @@ class OffsetFrom: class _AnnotationBase: xy: tuple[float, float] - xycoords: str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ] + xycoords: CoordsType def __init__( self, xy, - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., + xycoords: CoordsType = ..., annotation_clip: bool | None = ..., ) -> None: ... def set_annotation_clip(self, b: bool | None) -> None: ... @@ -147,17 +141,8 @@ class Annotation(Text, _AnnotationBase): text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = ..., - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., - textcoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | None = ..., + xycoords: CoordsType = ..., + textcoords: CoordsType | None = ..., arrowprops: dict[str, Any] | None = ..., annotation_clip: bool | None = ..., **kwargs @@ -165,17 +150,11 @@ class Annotation(Text, _AnnotationBase): @property def xycoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @xycoords.setter def xycoords( self, - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + xycoords: CoordsType, ) -> None: ... @property def xyann(self) -> tuple[float, float]: ... @@ -183,31 +162,19 @@ class Annotation(Text, _AnnotationBase): def xyann(self, xytext: tuple[float, float]) -> None: ... def get_anncoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... def set_anncoords( self, - coords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... @property def anncoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @anncoords.setter def anncoords( self, - coords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... def update_positions(self, renderer: RendererBase) -> None: ... # Drops `dpi` parameter from superclass diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index c00966d6e6c3..83182e3f5400 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -8,7 +8,7 @@ from matplotlib.font_manager import ( FontProperties, get_font, fontManager as _fontManager ) -from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT +from matplotlib.ft2font import LoadFlags from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.texmanager import TexManager @@ -37,7 +37,7 @@ def _get_font(self, prop): return font def _get_hinting_flag(self): - return LOAD_NO_HINTING + return LoadFlags.NO_HINTING def _get_char_id(self, font, ccode): """ @@ -61,7 +61,7 @@ def get_text_width_height_descent(self, s, prop, ismath): return width * scale, height * scale, descent * scale font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) + font.set_text(s, 0.0, flags=LoadFlags.NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 @@ -190,7 +190,7 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LOAD_NO_HINTING) + font.load_char(ccode, flags=LoadFlags.NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) @@ -241,11 +241,11 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, glyph_name_or_index = text.glyph_name_or_index if isinstance(glyph_name_or_index, str): index = font.get_name_index(glyph_name_or_index) - font.load_glyph(index, flags=LOAD_TARGET_LIGHT) + font.load_glyph(index, flags=LoadFlags.TARGET_LIGHT) elif isinstance(glyph_name_or_index, int): self._select_native_charmap(font) font.load_char( - glyph_name_or_index, flags=LOAD_TARGET_LIGHT) + glyph_name_or_index, flags=LoadFlags.TARGET_LIGHT) else: # Should not occur. raise TypeError(f"Glyph spec of unexpected type: " f"{glyph_name_or_index!r}") diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index d824bbe3b6e2..801359555a69 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -407,6 +407,11 @@ class ScalarFormatter(Formatter): useLocale : bool, default: :rc:`axes.formatter.use_locale`. Whether to use locale settings for decimal sign and positive sign. See `.set_useLocale`. + usetex : bool, default: :rc:`text.usetex` + To enable/disable the use of TeX's math mode for rendering the + numbers in the formatter. + + .. versionadded:: 3.10 Notes ----- @@ -444,13 +449,14 @@ class ScalarFormatter(Formatter): """ - def __init__(self, useOffset=None, useMathText=None, useLocale=None): + def __init__(self, useOffset=None, useMathText=None, useLocale=None, *, + usetex=None): if useOffset is None: useOffset = mpl.rcParams['axes.formatter.useoffset'] self._offset_threshold = \ mpl.rcParams['axes.formatter.offset_threshold'] self.set_useOffset(useOffset) - self._usetex = mpl.rcParams['text.usetex'] + self.set_usetex(usetex) self.set_useMathText(useMathText) self.orderOfMagnitude = 0 self.format = '' @@ -458,6 +464,14 @@ def __init__(self, useOffset=None, useMathText=None, useLocale=None): self._powerlimits = mpl.rcParams['axes.formatter.limits'] self.set_useLocale(useLocale) + def get_usetex(self): + return self._usetex + + def set_usetex(self, val): + self._usetex = mpl._val_or_rc(val, 'text.usetex') + + usetex = property(fget=get_usetex, fset=set_usetex) + def get_useOffset(self): """ Return whether automatic mode for offset notation is active. @@ -574,7 +588,7 @@ def set_useMathText(self, val): from matplotlib import font_manager ufont = font_manager.findfont( font_manager.FontProperties( - mpl.rcParams["font.family"] + family=mpl.rcParams["font.family"] ), fallback_to_default=False, ) @@ -987,13 +1001,7 @@ def set_locs(self, locs=None): self._sublabels = set(np.arange(1, b + 1)) def _num_to_string(self, x, vmin, vmax): - if x > 10000: - s = '%1.0e' % x - elif x < 1: - s = '%1.0e' % x - else: - s = self._pprint_val(x, vmax - vmin) - return s + return self._pprint_val(x, vmax - vmin) if 1 <= x <= 10000 else f"{x:1.0e}" def __call__(self, x, pos=None): # docstring inherited @@ -1053,15 +1061,14 @@ class LogFormatterExponent(LogFormatter): """ Format values for log axis using ``exponent = log_base(value)``. """ + def _num_to_string(self, x, vmin, vmax): fx = math.log(x) / math.log(self._base) - if abs(fx) > 10000: - s = '%1.0g' % fx - elif abs(fx) < 1: - s = '%1.0g' % fx - else: + if 1 <= abs(fx) <= 10000: fd = math.log(vmax - vmin) / math.log(self._base) s = self._pprint_val(fx, fd) + else: + s = f"{fx:1.0g}" return s @@ -1324,7 +1331,7 @@ def format_data_short(self, value): return f"1-{1 - value:e}" -class EngFormatter(Formatter): +class EngFormatter(ScalarFormatter): """ Format axis values using engineering prefixes to represent powers of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. @@ -1356,7 +1363,7 @@ class EngFormatter(Formatter): } def __init__(self, unit="", places=None, sep=" ", *, usetex=None, - useMathText=None): + useMathText=None, useOffset=False): r""" Parameters ---------- @@ -1390,76 +1397,124 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, useMathText : bool, default: :rc:`axes.formatter.use_mathtext` To enable/disable the use mathtext for rendering the numbers in the formatter. + useOffset : bool or float, default: False + Whether to use offset notation with :math:`10^{3*N}` based prefixes. + This features allows showing an offset with standard SI order of + magnitude prefix near the axis. Offset is computed similarly to + how `ScalarFormatter` computes it internally, but here you are + guaranteed to get an offset which will make the tick labels exceed + 3 digits. See also `.set_useOffset`. + + .. versionadded:: 3.10 """ self.unit = unit self.places = places self.sep = sep - self.set_usetex(usetex) - self.set_useMathText(useMathText) - - def get_usetex(self): - return self._usetex - - def set_usetex(self, val): - if val is None: - self._usetex = mpl.rcParams['text.usetex'] - else: - self._usetex = val - - usetex = property(fget=get_usetex, fset=set_usetex) + super().__init__( + useOffset=useOffset, + useMathText=useMathText, + useLocale=False, + usetex=usetex, + ) - def get_useMathText(self): - return self._useMathText + def __call__(self, x, pos=None): + """ + Return the format for tick value *x* at position *pos*. - def set_useMathText(self, val): - if val is None: - self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] + If there is no currently offset in the data, it returns the best + engineering formatting that fits the given argument, independently. + """ + if len(self.locs) == 0 or self.offset == 0: + return self.fix_minus(self.format_data(x)) else: - self._useMathText = val + xp = (x - self.offset) / (10. ** self.orderOfMagnitude) + if abs(xp) < 1e-8: + xp = 0 + return self._format_maybe_minus_and_locale(self.format, xp) - useMathText = property(fget=get_useMathText, fset=set_useMathText) + def set_locs(self, locs): + # docstring inherited + self.locs = locs + if len(self.locs) > 0: + vmin, vmax = sorted(self.axis.get_view_interval()) + if self._useOffset: + self._compute_offset() + if self.offset != 0: + # We don't want to use the offset computed by + # self._compute_offset because it rounds the offset unaware + # of our engineering prefixes preference, and this can + # cause ticks with 4+ digits to appear. These ticks are + # slightly less readable, so if offset is justified + # (decided by self._compute_offset) we set it to better + # value: + self.offset = round((vmin + vmax)/2, 3) + # Use log1000 to use engineers' oom standards + self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000))*3 + self._set_format() - def __call__(self, x, pos=None): - s = f"{self.format_eng(x)}{self.unit}" - # Remove the trailing separator when there is neither prefix nor unit - if self.sep and s.endswith(self.sep): - s = s[:-len(self.sep)] - return self.fix_minus(s) + # Simplify a bit ScalarFormatter.get_offset: We always want to use + # self.format_data. Also we want to return a non-empty string only if there + # is an offset, no matter what is self.orderOfMagnitude. If there _is_ an + # offset, self.orderOfMagnitude is consulted. This behavior is verified + # in `test_ticker.py`. + def get_offset(self): + # docstring inherited + if len(self.locs) == 0: + return '' + if self.offset: + offsetStr = '' + if self.offset: + offsetStr = self.format_data(self.offset) + if self.offset > 0: + offsetStr = '+' + offsetStr + sciNotStr = self.format_data(10 ** self.orderOfMagnitude) + if self._useMathText or self._usetex: + if sciNotStr != '': + sciNotStr = r'\times%s' % sciNotStr + s = f'${sciNotStr}{offsetStr}$' + else: + s = sciNotStr + offsetStr + return self.fix_minus(s) + return '' def format_eng(self, num): + """Alias to EngFormatter.format_data""" + return self.format_data(num) + + def format_data(self, value): """ Format a number in engineering notation, appending a letter representing the power of 1000 of the original number. Some examples: - >>> format_eng(0) # for self.places = 0 + >>> format_data(0) # for self.places = 0 '0' - >>> format_eng(1000000) # for self.places = 1 + >>> format_data(1000000) # for self.places = 1 '1.0 M' - >>> format_eng(-1e-6) # for self.places = 2 + >>> format_data(-1e-6) # for self.places = 2 '-1.00 \N{MICRO SIGN}' """ sign = 1 fmt = "g" if self.places is None else f".{self.places:d}f" - if num < 0: + if value < 0: sign = -1 - num = -num + value = -value - if num != 0: - pow10 = int(math.floor(math.log10(num) / 3) * 3) + if value != 0: + pow10 = int(math.floor(math.log10(value) / 3) * 3) else: pow10 = 0 - # Force num to zero, to avoid inconsistencies like + # Force value to zero, to avoid inconsistencies like # format_eng(-0) = "0" and format_eng(0.0) = "0" # but format_eng(-0.0) = "-0.0" - num = 0.0 + value = 0.0 pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) - mant = sign * num / (10.0 ** pow10) + mant = sign * value / (10.0 ** pow10) # Taking care of the cases like 999.9..., which may be rounded to 1000 # instead of 1 k. Beware of the corner case of values that are beyond # the range of SI prefixes (i.e. > 'Y'). @@ -1468,13 +1523,15 @@ def format_eng(self, num): mant /= 1000 pow10 += 3 - prefix = self.ENG_PREFIXES[int(pow10)] + unit_prefix = self.ENG_PREFIXES[int(pow10)] + if self.unit or unit_prefix: + suffix = f"{self.sep}{unit_prefix}{self.unit}" + else: + suffix = "" if self._usetex or self._useMathText: - formatted = f"${mant:{fmt}}${self.sep}{prefix}" + return f"${mant:{fmt}}${suffix}" else: - formatted = f"{mant:{fmt}}{self.sep}{prefix}" - - return formatted + return f"{mant:{fmt}}{suffix}" class PercentFormatter(Formatter): @@ -2275,8 +2332,7 @@ class LogLocator(Locator): Places ticks at the values ``subs[j] * base**i``. """ - @_api.delete_parameter("3.8", "numdecs") - def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): + def __init__(self, base=10.0, subs=(1.0,), *, numticks=None): """ Parameters ---------- @@ -2305,24 +2361,17 @@ def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): numticks = 'auto' self._base = float(base) self._set_subs(subs) - self._numdecs = numdecs self.numticks = numticks - @_api.delete_parameter("3.8", "numdecs") - def set_params(self, base=None, subs=None, numdecs=None, numticks=None): + def set_params(self, base=None, subs=None, *, numticks=None): """Set parameters within this locator.""" if base is not None: self._base = float(base) if subs is not None: self._set_subs(subs) - if numdecs is not None: - self._numdecs = numdecs if numticks is not None: self.numticks = numticks - numdecs = _api.deprecate_privatize_attribute( - "3.8", addendum="This attribute has no effect.") - def _set_subs(self, subs): """ Set the minor ticks for the log scaling every ``base**i*subs[j]``. @@ -2881,20 +2930,21 @@ class AutoMinorLocator(Locator): Place evenly spaced minor ticks, with the step size and maximum number of ticks chosen automatically. - The Axis scale must be linear with evenly spaced major ticks . + The Axis must use a linear scale and have evenly spaced major ticks. """ def __init__(self, n=None): """ - *n* is the number of subdivisions of the interval between - major ticks; e.g., n=2 will place a single minor tick midway - between major ticks. - - If *n* is omitted or None, the value stored in rcParams will be used. - In case *n* is set to 'auto', it will be set to 4 or 5. If the distance - between the major ticks equals 1, 2.5, 5 or 10 it can be perfectly - divided in 5 equidistant sub-intervals with a length multiple of - 0.05. Otherwise it is divided in 4 sub-intervals. + Parameters + ---------- + n : int or 'auto', default: :rc:`xtick.minor.ndivs` or :rc:`ytick.minor.ndivs` + The number of subdivisions of the interval between major ticks; + e.g., n=2 will place a single minor tick midway between major ticks. + + If *n* is 'auto', it will be set to 4 or 5: if the distance + between the major ticks equals 1, 2.5, 5 or 10 it can be perfectly + divided in 5 equidistant sub-intervals with a length multiple of + 0.05; otherwise, it is divided in 4 sub-intervals. """ self.ndivs = n diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index f026b4943c94..f990bf53ca42 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -64,8 +64,16 @@ class ScalarFormatter(Formatter): useOffset: bool | float | None = ..., useMathText: bool | None = ..., useLocale: bool | None = ..., + *, + usetex: bool | None = ..., ) -> None: ... offset: float + def get_usetex(self) -> bool: ... + def set_usetex(self, val: bool) -> None: ... + @property + def usetex(self) -> bool: ... + @usetex.setter + def usetex(self, val: bool) -> None: ... def get_useOffset(self) -> bool: ... def set_useOffset(self, val: bool | float) -> None: ... @property @@ -125,7 +133,7 @@ class LogitFormatter(Formatter): def set_minor_number(self, minor_number: int) -> None: ... def format_data_short(self, value: float) -> str: ... -class EngFormatter(Formatter): +class EngFormatter(ScalarFormatter): ENG_PREFIXES: dict[int, str] unit: str places: int | None @@ -137,20 +145,9 @@ class EngFormatter(Formatter): sep: str = ..., *, usetex: bool | None = ..., - useMathText: bool | None = ... + useMathText: bool | None = ..., + useOffset: bool | float | None = ..., ) -> None: ... - def get_usetex(self) -> bool: ... - def set_usetex(self, val: bool | None) -> None: ... - @property - def usetex(self) -> bool: ... - @usetex.setter - def usetex(self, val: bool | None) -> None: ... - def get_useMathText(self) -> bool: ... - def set_useMathText(self, val: bool | None) -> None: ... - @property - def useMathText(self) -> bool: ... - @useMathText.setter - def useMathText(self, val: bool | None) -> None: ... def format_eng(self, num: float) -> str: ... class PercentFormatter(Formatter): @@ -231,20 +228,19 @@ class MaxNLocator(Locator): def view_limits(self, dmin: float, dmax: float) -> tuple[float, float]: ... class LogLocator(Locator): - numdecs: float numticks: int | None def __init__( self, base: float = ..., subs: None | Literal["auto", "all"] | Sequence[float] = ..., - numdecs: float = ..., + *, numticks: int | None = ..., ) -> None: ... def set_params( self, base: float | None = ..., subs: Literal["auto", "all"] | Sequence[float] | None = ..., - numdecs: float | None = ..., + *, numticks: int | None = ..., ) -> None: ... diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 3575bd1fc14d..2934b0a77809 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -1,7 +1,6 @@ """ -Matplotlib includes a framework for arbitrary geometric -transformations that is used determine the final position of all -elements drawn on the canvas. +Matplotlib includes a framework for arbitrary geometric transformations that is used to +determine the final position of all elements drawn on the canvas. Transforms are composed into trees of `TransformNode` objects whose actual value depends on their children. When the contents of @@ -11,10 +10,10 @@ unnecessary recomputations of transforms, and contributes to better interactive performance. -For example, here is a graph of the transform tree used to plot data -to the graph: +For example, here is a graph of the transform tree used to plot data to the figure: -.. image:: ../_static/transforms.png +.. graphviz:: /api/transforms.dot + :alt: Diagram of transform tree from data to figure coordinates. The framework can be used for both affine and non-affine transformations. However, for speed, we want to use the backend @@ -38,6 +37,7 @@ import copy import functools +import itertools import textwrap import weakref import math @@ -92,9 +92,6 @@ class TransformNode: # Invalidation may affect only the affine part. If the # invalidation was "affine-only", the _invalid member is set to # INVALID_AFFINE_ONLY - INVALID_NON_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 1)) - INVALID_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 2)) - INVALID = _api.deprecated("3.8")(_api.classproperty(lambda cls: 3)) # Possible values for the _invalid attribute. _VALID, _INVALID_AFFINE_ONLY, _INVALID_FULL = range(3) @@ -479,7 +476,7 @@ def transformed(self, transform): 'NW': (0, 1.0), 'W': (0, 0.5)} - def anchored(self, c, container=None): + def anchored(self, c, container): """ Return a copy of the `Bbox` anchored to *c* within *container*. @@ -489,19 +486,13 @@ def anchored(self, c, container=None): Either an (*x*, *y*) pair of relative coordinates (0 is left or bottom, 1 is right or top), 'C' (center), or a cardinal direction ('SW', southwest, is bottom left, etc.). - container : `Bbox`, optional + container : `Bbox` The box within which the `Bbox` is positioned. See Also -------- .Axes.set_anchor """ - if container is None: - _api.warn_deprecated( - "3.8", message="Calling anchored() with no container bbox " - "returns a frozen copy of the original bbox and is deprecated " - "since %(since)s.") - container = self l, b, w, h = container.bounds L, B, W, H = self.bounds cx, cy = self.coefs[c] if isinstance(c, str) else c @@ -553,7 +544,7 @@ def splitx(self, *args): x0, y0, x1, y1 = self.extents w = x1 - x0 return [Bbox([[x0 + xf0 * w, y0], [x0 + xf1 * w, y1]]) - for xf0, xf1 in zip(xf[:-1], xf[1:])] + for xf0, xf1 in itertools.pairwise(xf)] def splity(self, *args): """ @@ -564,7 +555,7 @@ def splity(self, *args): x0, y0, x1, y1 = self.extents h = y1 - y0 return [Bbox([[x0, y0 + yf0 * h], [x1, y0 + yf1 * h]]) - for yf0, yf1 in zip(yf[:-1], yf[1:])] + for yf0, yf1 in itertools.pairwise(yf)] def count_contains(self, vertices): """ @@ -605,7 +596,6 @@ def expanded(self, sw, sh): a = np.array([[-deltaw, -deltah], [deltaw, deltah]]) return Bbox(self._points + a) - @_api.rename_parameter("3.8", "p", "w_pad") def padded(self, w_pad, h_pad=None): """ Construct a `Bbox` by padding this one on all four sides. @@ -1798,7 +1788,6 @@ def transform_affine(self, values): raise NotImplementedError('Affine subclasses should override this ' 'method.') - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited return values @@ -1856,7 +1845,6 @@ def to_values(self): mtx = self.get_matrix() return tuple(mtx[:2].swapaxes(0, 1).flat) - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): mtx = self.get_matrix() if isinstance(values, np.ma.MaskedArray): @@ -1867,7 +1855,6 @@ def transform_affine(self, values): if DEBUG: _transform_affine = transform_affine - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited # The major speed trap here is just converting to the @@ -2130,17 +2117,14 @@ def get_matrix(self): # docstring inherited return self._mtx - @_api.rename_parameter("3.8", "points", "values") def transform(self, values): # docstring inherited return np.asanyarray(values) - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited return np.asanyarray(values) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited return np.asanyarray(values) @@ -2229,7 +2213,6 @@ def frozen(self): # docstring inherited return blended_transform_factory(self._x.frozen(), self._y.frozen()) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited if self._x.is_affine and self._y.is_affine: @@ -2422,12 +2405,10 @@ def contains_branch_seperately(self, other_transform): __str__ = _make_str_method("_a", "_b") - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited return self.get_affine().transform(values) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited if self._a.is_affine and self._b.is_affine: @@ -2574,9 +2555,9 @@ def get_matrix(self): if DEBUG and (x_scale == 0 or y_scale == 0): raise ValueError( "Transforming from or to a singular bounding box") - self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale+outl)], - [0.0 , y_scale, (-inb*y_scale+outb)], - [0.0 , 0.0 , 1.0 ]], + self._mtx = np.array([[x_scale, 0.0, -inl*x_scale+outl], + [ 0.0, y_scale, -inb*y_scale+outb], + [ 0.0, 0.0, 1.0]], float) self._inverted = None self._invalid = 0 @@ -2668,9 +2649,9 @@ def get_matrix(self): raise ValueError("Transforming from a singular bounding box.") x_scale = 1.0 / inw y_scale = 1.0 / inh - self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale)], - [0.0 , y_scale, (-inb*y_scale)], - [0.0 , 0.0 , 1.0 ]], + self._mtx = np.array([[x_scale, 0.0, -inl*x_scale], + [ 0.0, y_scale, -inb*y_scale], + [ 0.0, 0.0, 1.0]], float) self._inverted = None self._invalid = 0 @@ -2703,6 +2684,25 @@ def get_matrix(self): return self._mtx +class _ScaledRotation(Affine2DBase): + """ + A transformation that applies rotation by *theta*, after transform by *trans_shift*. + """ + def __init__(self, theta, trans_shift): + super().__init__() + self._theta = theta + self._trans_shift = trans_shift + self._mtx = None + + def get_matrix(self): + if self._invalid: + transformed_coords = self._trans_shift.transform([[self._theta, 0]])[0] + adjusted_theta = transformed_coords[0] + rotation = Affine2D().rotate(adjusted_theta) + self._mtx = rotation.get_matrix() + return self._mtx + + class AffineDeltaTransform(Affine2DBase): r""" A transform wrapper for transforming displacements between pairs of points. @@ -2720,9 +2720,12 @@ class AffineDeltaTransform(Affine2DBase): This class is experimental as of 3.3, and the API may change. """ + pass_through = True + def __init__(self, transform, **kwargs): super().__init__(**kwargs) self._base_transform = transform + self.set_children(transform) __str__ = _make_str_method("_base_transform") diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 90a527e5bfc5..551487a11c60 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -77,9 +77,10 @@ class BboxBase(TransformNode): def fully_overlaps(self, other: BboxBase) -> bool: ... def transformed(self, transform: Transform) -> Bbox: ... coefs: dict[str, tuple[float, float]] - # anchored type can be s/str/Literal["C", "SW", "S", "SE", "E", "NE", "N", "NW", "W"] def anchored( - self, c: tuple[float, float] | str, container: BboxBase | None = ... + self, + c: tuple[float, float] | Literal['C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'], + container: BboxBase, ) -> Bbox: ... def shrunk(self, mx: float, my: float) -> Bbox: ... def shrunk_to_aspect( @@ -333,3 +334,8 @@ def offset_copy( y: float = ..., units: Literal["inches", "points", "dots"] = ..., ) -> Transform: ... + + +class _ScaledRotation(Affine2DBase): + def __init__(self, theta: float, trans_shift: Transform) -> None: ... + def get_matrix(self) -> np.ndarray: ... diff --git a/lib/matplotlib/tri/_tricontour.py b/lib/matplotlib/tri/_tricontour.py index 1db3715d01af..8250515f3ef8 100644 --- a/lib/matplotlib/tri/_tricontour.py +++ b/lib/matplotlib/tri/_tricontour.py @@ -5,7 +5,7 @@ from matplotlib.tri._triangulation import Triangulation -@_docstring.dedent_interpd +@_docstring.interpd class TriContourSet(ContourSet): """ Create and store a set of contour lines or filled regions for @@ -79,7 +79,7 @@ def _contour_args(self, args, kwargs): return (tri, z) -_docstring.interpd.update(_tricontour_doc=""" +_docstring.interpd.register(_tricontour_doc=""" Draw contour %%(type)s on an unstructured triangular grid. Call signatures:: @@ -218,7 +218,7 @@ def _contour_args(self, args, kwargs): @_docstring.Substitution(func='tricontour', type='lines') -@_docstring.dedent_interpd +@_docstring.interpd def tricontour(ax, *args, **kwargs): """ %(_tricontour_doc)s @@ -247,7 +247,7 @@ def tricontour(ax, *args, **kwargs): @_docstring.Substitution(func='tricontourf', type='regions') -@_docstring.dedent_interpd +@_docstring.interpd def tricontourf(ax, *args, **kwargs): """ %(_tricontour_doc)s diff --git a/lib/matplotlib/tri/_tripcolor.py b/lib/matplotlib/tri/_tripcolor.py index 1ac6c48a2d7c..f3c26b0b25ff 100644 --- a/lib/matplotlib/tri/_tripcolor.py +++ b/lib/matplotlib/tri/_tripcolor.py @@ -1,10 +1,11 @@ import numpy as np -from matplotlib import _api +from matplotlib import _api, _docstring from matplotlib.collections import PolyCollection, TriMesh from matplotlib.tri._triangulation import Triangulation +@_docstring.interpd def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None, vmax=None, shading='flat', facecolors=None, **kwargs): """ @@ -54,8 +55,25 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None, values used for each triangle are from the mean c of the triangle's three points. If *shading* is 'gouraud' then color values must be defined at points. - other_parameters - All other parameters are the same as for `~.Axes.pcolor`. + %(cmap_doc)s + + %(norm_doc)s + + %(vmin_vmax_doc)s + + %(colorizer_doc)s + + Returns + ------- + `~matplotlib.collections.PolyCollection` or `~matplotlib.collections.TriMesh` + The result depends on *shading*: For ``shading='flat'`` the result is a + `.PolyCollection`, for ``shading='gouraud'`` the result is a `.TriMesh`. + + Other Parameters + ---------------- + **kwargs : `~matplotlib.collections.Collection` properties + + %(Collection:kwdoc)s """ _api.check_in_list(['flat', 'gouraud'], shading=shading) diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 02059be94ba2..20e1022fa0a5 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -11,50 +11,68 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Literal, TypeVar, Union +from typing import Any, Callable, Literal, TypeAlias, TypeVar, Union from . import path from ._enums import JoinStyle, CapStyle +from .artist import Artist +from .backend_bases import RendererBase from .markers import MarkerStyle +from .transforms import Bbox, Transform -# The following are type aliases. Once python 3.9 is dropped, they should be annotated -# using ``typing.TypeAlias`` and Unions should be converted to using ``|`` syntax. - -RGBColorType = Union[tuple[float, float, float], str] -RGBAColorType = Union[ - str, # "none" or "#RRGGBBAA"/"#RGBA" hex strings - tuple[float, float, float, float], +RGBColorType: TypeAlias = tuple[float, float, float] | str +RGBAColorType: TypeAlias = ( + str | # "none" or "#RRGGBBAA"/"#RGBA" hex strings + tuple[float, float, float, float] | # 2 tuple (color, alpha) representations, not infinitely recursive # RGBColorType includes the (str, float) tuple, even for RGBA strings - tuple[RGBColorType, float], + tuple[RGBColorType, float] | # (4-tuple, float) is odd, but accepted as the outer float overriding A of 4-tuple - tuple[tuple[float, float, float, float], float], -] + tuple[tuple[float, float, float, float], float] +) -ColorType = Union[RGBColorType, RGBAColorType] +ColorType: TypeAlias = RGBColorType | RGBAColorType -RGBColourType = RGBColorType -RGBAColourType = RGBAColorType -ColourType = ColorType +RGBColourType: TypeAlias = RGBColorType +RGBAColourType: TypeAlias = RGBAColorType +ColourType: TypeAlias = ColorType -LineStyleType = Union[str, tuple[float, Sequence[float]]] -DrawStyleType = Literal["default", "steps", "steps-pre", "steps-mid", "steps-post"] -MarkEveryType = Union[ - None, int, tuple[int, int], slice, list[int], float, tuple[float, float], list[bool] -] +LineStyleType: TypeAlias = str | tuple[float, Sequence[float]] +DrawStyleType: TypeAlias = Literal["default", "steps", "steps-pre", "steps-mid", + "steps-post"] +MarkEveryType: TypeAlias = ( + None | + int | tuple[int, int] | slice | list[int] | + float | tuple[float, float] | + list[bool] +) -MarkerType = Union[str, path.Path, MarkerStyle] -FillStyleType = Literal["full", "left", "right", "bottom", "top", "none"] -JoinStyleType = Union[JoinStyle, Literal["miter", "round", "bevel"]] -CapStyleType = Union[CapStyle, Literal["butt", "projecting", "round"]] +MarkerType: TypeAlias = str | path.Path | MarkerStyle +FillStyleType: TypeAlias = Literal["full", "left", "right", "bottom", "top", "none"] +JoinStyleType: TypeAlias = JoinStyle | Literal["miter", "round", "bevel"] +CapStyleType: TypeAlias = CapStyle | Literal["butt", "projecting", "round"] -RcStyleType = Union[ +CoordsBaseType = Union[ str, - dict[str, Any], - pathlib.Path, - Sequence[Union[str, pathlib.Path, dict[str, Any]]], + Artist, + Transform, + Callable[ + [RendererBase], + Union[Bbox, Transform] + ] ] +CoordsType = Union[ + CoordsBaseType, + tuple[CoordsBaseType, CoordsBaseType] +] + +RcStyleType: TypeAlias = ( + str | + dict[str, Any] | + pathlib.Path | + Sequence[str | pathlib.Path | dict[str, Any]] +) _HT = TypeVar("_HT", bound=Hashable) -HashableList = list[Union[_HT, "HashableList[_HT]"]] +HashableList: TypeAlias = list[_HT | "HashableList[_HT]"] """A nested list of Hashable values.""" diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a298f3ae3d6a..9c676574310c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -117,7 +117,7 @@ def __init__(self, ax): self.ax = ax self._cids = [] - canvas = property(lambda self: self.ax.figure.canvas) + canvas = property(lambda self: self.ax.get_figure(root=True).canvas) def connect_event(self, event, callback): """ @@ -569,7 +569,7 @@ def set_val(self, val): self._handle.set_xdata([val]) self.valtext.set_text(self._format(val)) if self.drawon: - self.ax.figure.canvas.draw_idle() + self.ax.get_figure(root=True).canvas.draw_idle() self.val = val if self.eventson: self._observers.process('changed', val) @@ -945,7 +945,7 @@ def set_val(self, val): self.valtext.set_text(self._format((vmin, vmax))) if self.drawon: - self.ax.figure.canvas.draw_idle() + self.ax.get_figure(root=True).canvas.draw_idle() self.val = (vmin, vmax) if self.eventson: self._observers.process("changed", (vmin, vmax)) @@ -1370,8 +1370,9 @@ def _rendercursor(self): # This causes a single extra draw if the figure has never been rendered # yet, which should be fine as we're going to repeatedly re-render the # figure later anyways. - if self.ax.figure._get_renderer() is None: - self.ax.figure.canvas.draw() + fig = self.ax.get_figure(root=True) + if fig._get_renderer() is None: + fig.canvas.draw() text = self.text_disp.get_text() # Save value before overwriting it. widthtext = text[:self.cursor_index] @@ -1393,7 +1394,7 @@ def _rendercursor(self): visible=True) self.text_disp.set_text(text) - self.ax.figure.canvas.draw() + fig.canvas.draw() def _release(self, event): if self.ignore(event): @@ -1456,7 +1457,7 @@ def begin_typing(self): stack = ExitStack() # Register cleanup actions when user stops typing. self._on_stop_typing = stack.close toolmanager = getattr( - self.ax.figure.canvas.manager, "toolmanager", None) + self.ax.get_figure(root=True).canvas.manager, "toolmanager", None) if toolmanager is not None: # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. @@ -1478,7 +1479,7 @@ def stop_typing(self): notifysubmit = False self.capturekeystrokes = False self.cursor.set_visible(False) - self.ax.figure.canvas.draw() + self.ax.get_figure(root=True).canvas.draw() if notifysubmit and self.eventson: # Because process() might throw an error in the user's code, only # call it once we've already done our cleanup. @@ -1509,7 +1510,7 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - self.ax.figure.canvas.draw() + self.ax.get_figure(root=True).canvas.draw() def on_text_change(self, func): """ @@ -2003,7 +2004,8 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, self.vertOn = vertOn self._canvas_infos = { - ax.figure.canvas: {"cids": [], "background": None} for ax in axes} + ax.get_figure(root=True).canvas: + {"cids": [], "background": None} for ax in axes} xmin, xmax = axes[-1].get_xlim() ymin, ymax = axes[-1].get_ylim() @@ -2089,12 +2091,15 @@ def onmove(self, event): class _SelectorWidget(AxesWidget): - def __init__(self, ax, onselect, useblit=False, button=None, + def __init__(self, ax, onselect=None, useblit=False, button=None, state_modifier_keys=None, use_data_coordinates=False): super().__init__(ax) self._visible = True - self.onselect = onselect + if onselect is None: + self.onselect = lambda *args: None + else: + self.onselect = onselect self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() @@ -2201,7 +2206,7 @@ def ignore(self, event): def update(self): """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" if (not self.ax.get_visible() or - self.ax.figure._get_renderer() is None): + self.ax.get_figure(root=True)._get_renderer() is None): return if self.useblit: if self.background is not None: @@ -2345,11 +2350,6 @@ def get_visible(self): """Get the visibility of the selector artists.""" return self._visible - @property - def visible(self): - _api.warn_deprecated("3.8", alternative="get_visible") - return self.get_visible() - def clear(self): """Clear the selection and set the selector ready to make a new one.""" self._clear_without_update() @@ -2574,7 +2574,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, def new_axes(self, ax, *, _props=None, _init=False): """Set SpanSelector to operate on a new Axes.""" reconnect = False - if _init or self.canvas is not ax.figure.canvas: + if _init or self.canvas is not ax.get_figure(root=True).canvas: if self.canvas is not None: self.disconnect_events() reconnect = True @@ -2627,7 +2627,7 @@ def _set_cursor(self, enabled): else: cursor = backend_tools.Cursors.POINTER - self.ax.figure.canvas.set_cursor(cursor) + self.ax.get_figure(root=True).canvas.set_cursor(cursor) def connect_default_events(self): # docstring inherited @@ -3039,7 +3039,7 @@ def closest(self, x, y): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional A callback function that is called after a release event and the selection is created, changed or removed. It must have the signature:: @@ -3152,7 +3152,8 @@ class RectangleSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/rectangle_selector` """ - def __init__(self, ax, onselect, *, minspanx=0, minspany=0, useblit=False, + def __init__(self, ax, onselect=None, *, minspanx=0, + minspany=0, useblit=False, props=None, spancoords='data', button=None, grab_range=10, handle_props=None, interactive=False, state_modifier_keys=None, drag_from_anywhere=False, @@ -3674,7 +3675,7 @@ def onselect(verts): ---------- ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional Whenever the lasso is released, the *onselect* function is called and passed the vertices of the selected path. useblit : bool, default: True @@ -3689,7 +3690,7 @@ def onselect(verts): which corresponds to all buttons. """ - def __init__(self, ax, onselect, *, useblit=True, props=None, button=None): + def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None): super().__init__(ax, onselect, useblit=useblit, button=button) self.verts = None props = { @@ -3747,7 +3748,7 @@ class PolygonSelector(_SelectorWidget): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional When a polygon is completed or modified after completion, the *onselect* function is called and passed a list of the vertices as ``(xdata, ydata)`` tuples. @@ -3799,7 +3800,7 @@ class PolygonSelector(_SelectorWidget): point. """ - def __init__(self, ax, onselect, *, useblit=False, + def __init__(self, ax, onselect=None, *, useblit=False, props=None, handle_props=None, grab_range=10, draw_bounding_box=False, box_handle_props=None, box_props=None): @@ -3849,7 +3850,6 @@ def _get_bbox(self): def _add_box(self): self._box = RectangleSelector(self.ax, - onselect=lambda *args, **kwargs: None, useblit=self.useblit, grab_range=self.grab_range, handle_props=self._box_handle_props, @@ -3971,11 +3971,17 @@ def onmove(self, event): # needs to process the move callback even if there is no button press. # _SelectorWidget.onmove include logic to ignore move event if # _eventpress is None. - if not self.ignore(event): + if self.ignore(event): + # Hide the cursor when interactive zoom/pan is active + if not self.canvas.widgetlock.available(self) and self._xys: + self._xys[-1] = (np.nan, np.nan) + self._draw_polygon() + return False + + else: event = self._clean_event(event) self._onmove(event) return True - return False def _onmove(self, event): """Cursor move event handler.""" diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index 58adf85aae60..0fcd1990e17e 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -4,7 +4,7 @@ from .backend_bases import FigureCanvasBase, Event, MouseEvent, MouseButton from .collections import LineCollection from .figure import Figure from .lines import Line2D -from .patches import Circle, Polygon, Rectangle +from .patches import Polygon, Rectangle from .text import Text import PIL.Image @@ -276,7 +276,7 @@ class _SelectorWidget(AxesWidget): def __init__( self, ax: Axes, - onselect: Callable[[float, float], Any], + onselect: Callable[[float, float], Any] | None = ..., useblit: bool = ..., button: MouseButton | Collection[MouseButton] | None = ..., state_modifier_keys: dict[str, str] | None = ..., @@ -294,8 +294,6 @@ class _SelectorWidget(AxesWidget): def on_key_release(self, event: Event) -> None: ... def set_visible(self, visible: bool) -> None: ... def get_visible(self) -> bool: ... - @property - def visible(self) -> bool: ... def clear(self) -> None: ... @property def artists(self) -> tuple[Artist]: ... @@ -403,7 +401,7 @@ class RectangleSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[MouseEvent, MouseEvent], Any], + onselect: Callable[[MouseEvent, MouseEvent], Any] | None = ..., *, minspanx: float = ..., minspany: float = ..., @@ -443,7 +441,7 @@ class LassoSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[list[tuple[float, float]]], Any], + onselect: Callable[[list[tuple[float, float]]], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., @@ -455,7 +453,7 @@ class PolygonSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[ArrayLike, ArrayLike], Any], + onselect: Callable[[ArrayLike, ArrayLike], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index 1238310b462b..214b15843ebf 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -1,12 +1,12 @@ -from matplotlib import _api, transforms +from matplotlib import transforms from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, DrawingArea, TextArea, VPacker) -from matplotlib.patches import (Rectangle, Ellipse, ArrowStyle, +from matplotlib.patches import (Rectangle, ArrowStyle, FancyArrowPatch, PathPatch) from matplotlib.text import TextPath __all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox', - 'AnchoredEllipse', 'AnchoredSizeBar', 'AnchoredDirectionArrows'] + 'AnchoredSizeBar', 'AnchoredDirectionArrows'] class AnchoredDrawingArea(AnchoredOffsetbox): @@ -124,54 +124,6 @@ def __init__(self, transform, loc, **kwargs) -@_api.deprecated("3.8") -class AnchoredEllipse(AnchoredOffsetbox): - def __init__(self, transform, width, height, angle, loc, - pad=0.1, borderpad=0.1, prop=None, frameon=True, **kwargs): - """ - Draw an anchored ellipse of a given size. - - Parameters - ---------- - transform : `~matplotlib.transforms.Transform` - The transformation object for the coordinate system in use, i.e., - :attr:`matplotlib.axes.Axes.transData`. - width, height : float - Width and height of the ellipse, given in coordinates of - *transform*. - angle : float - Rotation of the ellipse, in degrees, anti-clockwise. - loc : str - Location of the ellipse. Valid locations are - 'upper left', 'upper center', 'upper right', - 'center left', 'center', 'center right', - 'lower left', 'lower center', 'lower right'. - For backward compatibility, numeric values are accepted as well. - See the parameter *loc* of `.Legend` for details. - pad : float, default: 0.1 - Padding around the ellipse, in fraction of the font size. - borderpad : float, default: 0.1 - Border padding, in fraction of the font size. - frameon : bool, default: True - If True, draw a box around the ellipse. - prop : `~matplotlib.font_manager.FontProperties`, optional - Font property used as a reference for paddings. - **kwargs - Keyword arguments forwarded to `.AnchoredOffsetbox`. - - Attributes - ---------- - ellipse : `~matplotlib.patches.Ellipse` - Ellipse patch drawn. - """ - self._box = AuxTransformBox(transform) - self.ellipse = Ellipse((0, 0), width, height, angle=angle) - self._box.add_artist(self.ellipse) - - super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box, - prop=prop, frameon=frameon, **kwargs) - - class AnchoredSizeBar(AnchoredOffsetbox): def __init__(self, transform, size, label, loc, pad=0.1, borderpad=0.1, sep=2, diff --git a/lib/mpl_toolkits/axes_grid1/axes_divider.py b/lib/mpl_toolkits/axes_grid1/axes_divider.py index f6c38f35dbc4..50365f482b72 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_divider.py +++ b/lib/mpl_toolkits/axes_grid1/axes_divider.py @@ -199,31 +199,6 @@ def new_locator(self, nx, ny, nx1=None, ny1=None): locator.get_subplotspec = self.get_subplotspec return locator - @_api.deprecated( - "3.8", alternative="divider.new_locator(...)(ax, renderer)") - def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): - """ - Implementation of ``divider.new_locator().__call__``. - - Parameters - ---------- - nx, nx1 : int - Integers specifying the column-position of the cell. When *nx1* is - None, a single *nx*-th column is specified. Otherwise, the - location of columns spanning between *nx* to *nx1* (but excluding - *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - axes - renderer - """ - xref = self._xrefindex - yref = self._yrefindex - return self._locate( - nx - xref, (nx + 1 if nx1 is None else nx1) - xref, - ny - yref, (ny + 1 if ny1 is None else ny1) - yref, - axes, renderer) - def _locate(self, nx, ny, nx1, ny1, axes, renderer): """ Implementation of ``divider.new_locator().__call__``. @@ -305,57 +280,6 @@ def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None): self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad) -@_api.deprecated("3.8") -class AxesLocator: - """ - A callable object which returns the position and size of a given - `.AxesDivider` cell. - """ - - def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): - """ - Parameters - ---------- - axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider` - nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise, location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - """ - self._axes_divider = axes_divider - - _xrefindex = axes_divider._xrefindex - _yrefindex = axes_divider._yrefindex - - self._nx, self._ny = nx - _xrefindex, ny - _yrefindex - - if nx1 is None: - nx1 = len(self._axes_divider) - if ny1 is None: - ny1 = len(self._axes_divider[0]) - - self._nx1 = nx1 - _xrefindex - self._ny1 = ny1 - _yrefindex - - def __call__(self, axes, renderer): - - _xrefindex = self._axes_divider._xrefindex - _yrefindex = self._axes_divider._yrefindex - - return self._axes_divider.locate(self._nx + _xrefindex, - self._ny + _yrefindex, - self._nx1 + _xrefindex, - self._ny1 + _yrefindex, - axes, - renderer) - - def get_subplotspec(self): - return self._axes_divider.get_subplotspec() - - class SubplotDivider(Divider): """ The Divider class whose rectangle area is specified as a subplot geometry. diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index 315a7bccd668..20abf18ea79c 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -17,14 +17,9 @@ def __init__(self, *args, orientation, **kwargs): super().__init__(*args, **kwargs) def colorbar(self, mappable, **kwargs): - return self.figure.colorbar( + return self.get_figure(root=False).colorbar( mappable, cax=self, location=self.orientation, **kwargs) - @_api.deprecated("3.8", alternative="ax.tick_params and colorbar.set_label") - def toggle_label(self, b): - axis = self.axis[self.orientation] - axis.toggle(ticklabels=b, label=b) - _cbaraxes_class_factory = cbook._make_class_factory(CbarAxesBase, "Cbar{}") @@ -358,6 +353,11 @@ def __init__(self, fig, cbar_location : {"left", "right", "bottom", "top"}, default: "right" cbar_pad : float, default: None Padding between the image axes and the colorbar axes. + + .. versionchanged:: 3.10 + ``cbar_mode="single"`` no longer adds *axes_pad* between the axes + and the colorbar if the *cbar_location* is "left" or "bottom". + cbar_size : size specification (see `.Size.from_any`), default: "5%" Colorbar size. cbar_set_cax : bool, default: True @@ -410,7 +410,7 @@ def _init_locators(self): self._colorbar_pad = self._vert_pad_size.fixed_size self.cbar_axes = [ _cbaraxes_class_factory(self._defaultAxesClass)( - self.axes_all[0].figure, self._divider.get_position(), + self.axes_all[0].get_figure(root=False), self._divider.get_position(), orientation=self._colorbar_location) for _ in range(self.ngrids)] @@ -439,7 +439,7 @@ def _init_locators(self): self.cbar_axes[0].set_visible(True) for col, ax in enumerate(self.axes_row[0]): - if h: + if col != 0: h.append(self._horiz_pad_size) if ax: @@ -468,7 +468,7 @@ def _init_locators(self): v_ax_pos = [] v_cb_pos = [] for row, ax in enumerate(self.axes_column[0][::-1]): - if v: + if row != 0: v.append(self._vert_pad_size) if ax: diff --git a/lib/mpl_toolkits/axes_grid1/axes_size.py b/lib/mpl_toolkits/axes_grid1/axes_size.py index e417c1a899ac..86e5f70d9824 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_size.py +++ b/lib/mpl_toolkits/axes_grid1/axes_size.py @@ -7,6 +7,10 @@ class (or others) to determine the size of each Axes. The unit Note that this class is nothing more than a simple tuple of two floats. Take a look at the Divider class to see how these two values are used. + +Once created, the unit classes can be modified by simple arithmetic +operations: addition /subtraction with another unit type or a real number and scaling +(multiplication or division) by a real number. """ from numbers import Real @@ -17,14 +21,33 @@ class (or others) to determine the size of each Axes. The unit class _Base: def __rmul__(self, other): + return self * other + + def __mul__(self, other): + if not isinstance(other, Real): + return NotImplemented return Fraction(other, self) + def __div__(self, other): + return (1 / other) * self + def __add__(self, other): if isinstance(other, _Base): return Add(self, other) else: return Add(self, Fixed(other)) + def __neg__(self): + return -1 * self + + def __radd__(self, other): + # other cannot be a _Base instance, because A + B would trigger + # A.__add__(B) first. + return Add(self, Fixed(other)) + + def __sub__(self, other): + return self + (-other) + def get_size(self, renderer): """ Return two-float tuple with relative and absolute sizes. diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 6d591a45311b..52fe6efc0618 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -6,58 +6,13 @@ from matplotlib.offsetbox import AnchoredOffsetbox from matplotlib.patches import Patch, Rectangle from matplotlib.path import Path -from matplotlib.transforms import Bbox, BboxTransformTo +from matplotlib.transforms import Bbox from matplotlib.transforms import IdentityTransform, TransformedBbox from . import axes_size as Size from .parasite_axes import HostAxes -@_api.deprecated("3.8", alternative="Axes.inset_axes") -class InsetPosition: - @_docstring.dedent_interpd - def __init__(self, parent, lbwh): - """ - An object for positioning an inset axes. - - This is created by specifying the normalized coordinates in the axes, - instead of the figure. - - Parameters - ---------- - parent : `~matplotlib.axes.Axes` - Axes to use for normalizing coordinates. - - lbwh : iterable of four floats - The left edge, bottom edge, width, and height of the inset axes, in - units of the normalized coordinate of the *parent* axes. - - See Also - -------- - :meth:`matplotlib.axes.Axes.set_axes_locator` - - Examples - -------- - The following bounds the inset axes to a box with 20%% of the parent - axes height and 40%% of the width. The size of the axes specified - ([0, 0, 1, 1]) ensures that the axes completely fills the bounding box: - - >>> parent_axes = plt.gca() - >>> ax_ins = plt.axes([0, 0, 1, 1]) - >>> ip = InsetPosition(parent_axes, [0.5, 0.1, 0.4, 0.2]) - >>> ax_ins.set_axes_locator(ip) - """ - self.parent = parent - self.lbwh = lbwh - - def __call__(self, ax, renderer): - bbox_parent = self.parent.get_position(original=False) - trans = BboxTransformTo(bbox_parent) - bbox_inset = Bbox.from_bounds(*self.lbwh) - bb = TransformedBbox(bbox_inset, trans) - return bb - - class AnchoredLocatorBase(AnchoredOffsetbox): def __init__(self, bbox_to_anchor, offsetbox, loc, borderpad=0.5, bbox_transform=None): @@ -70,13 +25,14 @@ def draw(self, renderer): raise RuntimeError("No draw method should be called") def __call__(self, ax, renderer): + fig = ax.get_figure(root=False) if renderer is None: - renderer = ax.figure._get_renderer() + renderer = fig._get_renderer() self.axes = ax bbox = self.get_window_extent(renderer) px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer) bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height) - tr = ax.figure.transSubfigure.inverted() + tr = fig.transSubfigure.inverted() return TransformedBbox(bbox_canvas, tr) @@ -130,7 +86,7 @@ def get_bbox(self, renderer): class BboxPatch(Patch): - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox, **kwargs): """ Patch showing the shape bounded by a Bbox. @@ -192,7 +148,7 @@ def connect_bbox(bbox1, bbox2, loc1, loc2=None): x2, y2 = BboxConnector.get_bbox_edge_pos(bbox2, loc2) return Path([[x1, y1], [x2, y2]]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox1, bbox2, loc1, loc2=None, **kwargs): """ Connect two bboxes with a straight line. @@ -236,7 +192,7 @@ def get_path(self): class BboxConnectorPatch(BboxConnector): - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): """ Connect two bboxes with a quadrilateral. @@ -287,13 +243,14 @@ def _add_inset_axes(parent_axes, axes_class, axes_kwargs, axes_locator): axes_class = HostAxes if axes_kwargs is None: axes_kwargs = {} + fig = parent_axes.get_figure(root=False) inset_axes = axes_class( - parent_axes.figure, parent_axes.get_position(), + fig, parent_axes.get_position(), **{"navigate": False, **axes_kwargs, "axes_locator": axes_locator}) - return parent_axes.figure.add_axes(inset_axes) + return fig.add_axes(inset_axes) -@_docstring.dedent_interpd +@_docstring.interpd def inset_axes(parent_axes, width, height, loc='upper right', bbox_to_anchor=None, bbox_transform=None, axes_class=None, axes_kwargs=None, @@ -395,7 +352,8 @@ def inset_axes(parent_axes, width, height, loc='upper right', Inset axes object created. """ - if (bbox_transform in [parent_axes.transAxes, parent_axes.figure.transFigure] + if (bbox_transform in [parent_axes.transAxes, + parent_axes.get_figure(root=False).transFigure] and bbox_to_anchor is None): _api.warn_external("Using the axes or figure transform requires a " "bounding box in the respective coordinates. " @@ -416,7 +374,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', bbox_transform=bbox_transform, borderpad=borderpad)) -@_docstring.dedent_interpd +@_docstring.interpd def zoomed_inset_axes(parent_axes, zoom, loc='upper right', bbox_to_anchor=None, bbox_transform=None, axes_class=None, axes_kwargs=None, @@ -509,7 +467,7 @@ def get_points(self): return super().get_points() -@_docstring.dedent_interpd +@_docstring.interpd def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): """ Draw a box to mark the location of an area represented by an inset axes. diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index 2a2b5957e844..f7bc2df6d7e0 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -13,7 +13,8 @@ def __init__(self, parent_axes, aux_transform=None, self.transAux = aux_transform self.set_viewlim_mode(viewlim_mode) kwargs["frameon"] = False - super().__init__(parent_axes.figure, parent_axes._position, **kwargs) + super().__init__(parent_axes.get_figure(root=False), + parent_axes._position, **kwargs) def clear(self): super().clear() @@ -215,8 +216,7 @@ def _remove_any_twin(self, ax): self.axis[tuple(restore)].set_visible(True) self.axis[tuple(restore)].toggle(ticklabels=False, label=False) - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, bbox_extra_artists=None): bbs = [ *[ax.get_tightbbox(renderer, call_axes_locator=call_axes_locator) diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png deleted file mode 100644 index e8676cfd6c95..000000000000 Binary files a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png and /dev/null differ diff --git a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index 7c444f6ae178..778bd9ca04d0 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -18,15 +18,14 @@ host_subplot, make_axes_locatable, Grid, AxesGrid, ImageGrid) from mpl_toolkits.axes_grid1.anchored_artists import ( - AnchoredAuxTransformBox, AnchoredDrawingArea, AnchoredEllipse, + AnchoredAuxTransformBox, AnchoredDrawingArea, AnchoredDirectionArrows, AnchoredSizeBar) from mpl_toolkits.axes_grid1.axes_divider import ( Divider, HBoxDivider, make_axes_area_auto_adjustable, SubplotDivider, VBoxDivider) from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes from mpl_toolkits.axes_grid1.inset_locator import ( - zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch, - InsetPosition) + zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch) import mpl_toolkits.axes_grid1.mpl_axes import pytest @@ -424,7 +423,7 @@ def test_image_grid_single_bottom(): fig = plt.figure(1, (2.5, 1.5)) grid = ImageGrid(fig, (0, 0, 1, 1), nrows_ncols=(1, 3), - axes_pad=(0.2, 0.15), cbar_mode="single", + axes_pad=(0.2, 0.15), cbar_mode="single", cbar_pad=0.3, cbar_location="bottom", cbar_size="10%", label_mode="1") # 4-tuple rect => Divider, isinstance will give True for SubplotDivider assert type(grid.get_divider()) is Divider @@ -515,7 +514,7 @@ def on_pick(event): if click_axes is axes["parasite"]: click_axes = axes["host"] (x, y) = click_axes.transAxes.transform(axes_coords) - m = MouseEvent("button_press_event", click_axes.figure.canvas, x, y, + m = MouseEvent("button_press_event", click_axes.get_figure(root=True).canvas, x, y, button=1) click_axes.pick(m) # Checks @@ -543,12 +542,14 @@ def test_anchored_artists(): box.drawing_area.add_artist(el) ax.add_artist(box) - # Manually construct the ellipse instead, once the deprecation elapses. - with pytest.warns(mpl.MatplotlibDeprecationWarning): - ae = AnchoredEllipse(ax.transData, width=0.1, height=0.25, angle=-60, - loc='lower left', pad=0.5, borderpad=0.4, - frameon=True) - ax.add_artist(ae) + # This block used to test the AnchoredEllipse class, but that was removed. The block + # remains, though it duplicates the above ellipse, so that the test image doesn't + # need to be regenerated. + box = AnchoredAuxTransformBox(ax.transData, loc='lower left', frameon=True, + pad=0.5, borderpad=0.4) + el = Ellipse((0, 0), width=0.1, height=0.25, angle=-60) + box.drawing_area.add_artist(el) + ax.add_artist(box) asb = AnchoredSizeBar(ax.transData, 0.2, r"0.2 units", loc='lower right', pad=0.3, borderpad=0.4, sep=4, fill_bar=True, @@ -702,17 +703,6 @@ def test_rgb_axes(): ax.imshow_rgb(r, g, b, interpolation='none') -# Update style when regenerating the test image -@image_comparison(['insetposition.png'], remove_text=True, - style=('classic', '_classic_test_patch')) -def test_insetposition(): - fig, ax = plt.subplots(figsize=(2, 2)) - ax_ins = plt.axes([0, 0, 1, 1]) - with pytest.warns(mpl.MatplotlibDeprecationWarning): - ip = InsetPosition(ax, [0.2, 0.25, 0.5, 0.4]) - ax_ins.set_axes_locator(ip) - - # The original version of this test relied on mpl_toolkits's slightly different # colorbar implementation; moving to matplotlib's own colorbar implementation # caused the small image comparison error. diff --git a/lib/mpl_toolkits/axisartist/axes_divider.py b/lib/mpl_toolkits/axisartist/axes_divider.py index a01d4e27df93..d0392be782d9 100644 --- a/lib/mpl_toolkits/axisartist/axes_divider.py +++ b/lib/mpl_toolkits/axisartist/axes_divider.py @@ -1,2 +1,2 @@ from mpl_toolkits.axes_grid1.axes_divider import ( # noqa - Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable) + Divider, SubplotDivider, AxesDivider, make_axes_locatable) diff --git a/lib/mpl_toolkits/axisartist/axes_grid.py b/lib/mpl_toolkits/axisartist/axes_grid.py deleted file mode 100644 index ecb3e9d92c18..000000000000 --- a/lib/mpl_toolkits/axisartist/axes_grid.py +++ /dev/null @@ -1,23 +0,0 @@ -from matplotlib import _api - -import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig -from .axislines import Axes - - -_api.warn_deprecated( - "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_grid") - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_grid.Grid(..., axes_class=axislines.Axes")) -class Grid(axes_grid_orig.Grid): - _defaultAxesClass = Axes - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_grid.ImageGrid(..., axes_class=axislines.Axes")) -class ImageGrid(axes_grid_orig.ImageGrid): - _defaultAxesClass = Axes - - -AxesGrid = ImageGrid diff --git a/lib/mpl_toolkits/axisartist/axes_rgb.py b/lib/mpl_toolkits/axisartist/axes_rgb.py deleted file mode 100644 index 2195747469a1..000000000000 --- a/lib/mpl_toolkits/axisartist/axes_rgb.py +++ /dev/null @@ -1,18 +0,0 @@ -from matplotlib import _api -from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa - make_rgb_axes, RGBAxes as _RGBAxes) -from .axislines import Axes - - -_api.warn_deprecated( - "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_rgb") - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_rgb.RGBAxes(..., axes_class=axislines.Axes")) -class RGBAxes(_RGBAxes): - """ - Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with - ``_defaultAxesClass`` = `.axislines.Axes`. - """ - _defaultAxesClass = Axes diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 407ad07a3dc2..b416d56abe6b 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -253,7 +253,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() # save original and adjust some properties tr = self.get_transform() @@ -312,13 +312,13 @@ def get_pad(self): def get_ref_artist(self): # docstring inherited - return self._axis.get_label() + return self._axis.label def get_text(self): # docstring inherited t = super().get_text() if t == "__from_axes__": - return self._axis.get_label().get_text() + return self._axis.label.get_text() return self._text _default_alignments = dict(left=("bottom", "center"), @@ -391,7 +391,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): return @@ -550,7 +550,7 @@ def set_locs_angles_labels(self, locs_angles_labels): def get_window_extents(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): self._axislabel_pad = self._external_pad @@ -691,7 +691,7 @@ def __init__(self, axes, self.offset_transform = ScaledTranslation( *offset, Affine2D().scale(1 / 72) # points to inches. - + self.axes.figure.dpi_scale_trans) + + self.axes.get_figure(root=False).dpi_scale_trans) if axis_direction in ["left", "right"]: self.axis = axes.yaxis @@ -879,7 +879,7 @@ def _init_ticks(self, **kwargs): self.major_ticklabels = TickLabels( axis=self.axis, axis_direction=self._axis_direction, - figure=self.axes.figure, + figure=self.axes.get_figure(root=False), transform=trans, fontsize=size, pad=kwargs.get( @@ -888,7 +888,7 @@ def _init_ticks(self, **kwargs): self.minor_ticklabels = TickLabels( axis=self.axis, axis_direction=self._axis_direction, - figure=self.axes.figure, + figure=self.axes.get_figure(root=False), transform=trans, fontsize=size, pad=kwargs.get( @@ -922,7 +922,7 @@ def _update_ticks(self, renderer=None): # majorticks even for minor ticks. not clear what is best. if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() dpi_cor = renderer.points_to_pixels(1.) if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): @@ -997,7 +997,7 @@ def _init_label(self, **kwargs): transform=tr, axis_direction=self._axis_direction, ) - self.label.set_figure(self.axes.figure) + self.label.set_figure(self.axes.get_figure(root=False)) labelpad = kwargs.get("labelpad", 5) self.label.set_pad(labelpad) diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index 1d695c129ae2..8d06cb236269 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -370,10 +370,6 @@ def get_gridlines(self, which="major", axis="both"): class Axes(maxes.Axes): - @_api.deprecated("3.8", alternative="ax.axis") - def __call__(self, *args, **kwargs): - return maxes.Axes.axis(self.axes, *args, **kwargs) - def __init__(self, *args, grid_helper=None, **kwargs): self._axisline_on = True self._grid_helper = grid_helper if grid_helper else GridHelperRectlinear(self) diff --git a/lib/mpl_toolkits/axisartist/floating_axes.py b/lib/mpl_toolkits/axisartist/floating_axes.py index 24c9ce61afa7..74e4c941879b 100644 --- a/lib/mpl_toolkits/axisartist/floating_axes.py +++ b/lib/mpl_toolkits/axisartist/floating_axes.py @@ -147,17 +147,6 @@ def __init__(self, aux_trans, extremes, tick_formatter1=tick_formatter1, tick_formatter2=tick_formatter2) - @_api.deprecated("3.8") - def get_data_boundary(self, side): - """ - Return v=0, nth=1. - """ - lon1, lon2, lat1, lat2 = self.grid_finder.extreme_finder(*[None] * 5) - return dict(left=(lon1, 0), - right=(lon2, 0), - bottom=(lat1, 1), - top=(lat2, 1))[side] - def new_fixed_axis( self, loc, nth_coord=None, axis_direction=None, offset=None, axes=None): if axes is None: @@ -266,7 +255,7 @@ def clear(self): # The original patch is not in the draw tree; it is only used for # clipping purposes. orig_patch = super()._gen_axes_patch() - orig_patch.set_figure(self.figure) + orig_patch.set_figure(self.get_figure(root=False)) orig_patch.set_transform(self.transAxes) self.patch.set_clip_path(orig_patch) self.gridlines.set_clip_path(orig_patch) diff --git a/lib/mpl_toolkits/axisartist/meson.build b/lib/mpl_toolkits/axisartist/meson.build index 8d9314e42576..6d95cf0dfdcd 100644 --- a/lib/mpl_toolkits/axisartist/meson.build +++ b/lib/mpl_toolkits/axisartist/meson.build @@ -2,8 +2,6 @@ python_sources = [ '__init__.py', 'angle_helper.py', 'axes_divider.py', - 'axes_grid.py', - 'axes_rgb.py', 'axis_artist.py', 'axislines.py', 'axisline_style.py', diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 9c98f13c6e43..deb0ca34302c 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -14,7 +14,7 @@ from contextlib import contextmanager from matplotlib import ( - artist, cbook, colors as mcolors, lines, text as mtext, + _api, artist, cbook, colors as mcolors, lines, text as mtext, path as mpath) from matplotlib.collections import ( Collection, LineCollection, PolyCollection, PatchCollection, PathCollection) @@ -73,6 +73,34 @@ def get_dir_vector(zdir): raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") +def _viewlim_mask(xs, ys, zs, axes): + """ + Return original points with points outside the axes view limits masked. + + Parameters + ---------- + xs, ys, zs : array-like + The points to mask. + axes : Axes3D + The axes to use for the view limits. + + Returns + ------- + xs_masked, ys_masked, zs_masked : np.ma.array + The masked points. + """ + mask = np.logical_or.reduce((xs < axes.xy_viewLim.xmin, + xs > axes.xy_viewLim.xmax, + ys < axes.xy_viewLim.ymin, + ys > axes.xy_viewLim.ymax, + zs < axes.zz_viewLim.xmin, + zs > axes.zz_viewLim.xmax)) + xs_masked = np.ma.array(xs, mask=mask) + ys_masked = np.ma.array(ys, mask=mask) + zs_masked = np.ma.array(zs, mask=mask) + return xs_masked, ys_masked, zs_masked + + class Text3D(mtext.Text): """ Text object with 3D position and direction. @@ -86,6 +114,8 @@ class Text3D(mtext.Text): zdir : {'x', 'y', 'z', None, 3-tuple} The direction of the text. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. Other Parameters ---------------- @@ -93,9 +123,10 @@ class Text3D(mtext.Text): All other parameters are passed on to `~matplotlib.text.Text`. """ - def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): + def __init__(self, x=0, y=0, z=0, text='', zdir='z', axlim_clip=False, + **kwargs): mtext.Text.__init__(self, x, y, text, **kwargs) - self.set_3d_properties(z, zdir) + self.set_3d_properties(z, zdir, axlim_clip) def get_position_3d(self): """Return the (x, y, z) position of the text.""" @@ -129,7 +160,7 @@ def set_z(self, z): self._z = z self.stale = True - def set_3d_properties(self, z=0, zdir='z'): + def set_3d_properties(self, z=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the text. @@ -140,14 +171,23 @@ def set_3d_properties(self, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ self._z = z self._dir_vec = get_dir_vector(zdir) + self._axlim_clip = axlim_clip self.stale = True @artist.allow_rasterization def draw(self, renderer): - position3d = np.array((self._x, self._y, self._z)) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(self._x, self._y, self._z, self.axes) + position3d = np.ma.row_stack((xs, ys, zs)).ravel().filled(np.nan) + else: + xs, ys, zs = self._x, self._y, self._z + position3d = np.asanyarray([xs, ys, zs]) + proj = proj3d._proj_trans_points( [position3d, position3d + self._dir_vec], self.axes.M) dx = proj[0][1] - proj[0][0] @@ -164,7 +204,7 @@ def get_tightbbox(self, renderer=None): return None -def text_2d_to_3d(obj, z=0, zdir='z'): +def text_2d_to_3d(obj, z=0, zdir='z', axlim_clip=False): """ Convert a `.Text` to a `.Text3D` object. @@ -175,9 +215,11 @@ def text_2d_to_3d(obj, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ obj.__class__ = Text3D - obj.set_3d_properties(z, zdir) + obj.set_3d_properties(z, zdir, axlim_clip) class Line3D(lines.Line2D): @@ -191,7 +233,7 @@ class Line3D(lines.Line2D): `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`. """ - def __init__(self, xs, ys, zs, *args, **kwargs): + def __init__(self, xs, ys, zs, *args, axlim_clip=False, **kwargs): """ Parameters @@ -207,8 +249,9 @@ def __init__(self, xs, ys, zs, *args, **kwargs): """ super().__init__([], [], *args, **kwargs) self.set_data_3d(xs, ys, zs) + self._axlim_clip = axlim_clip - def set_3d_properties(self, zs=0, zdir='z'): + def set_3d_properties(self, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the line. @@ -220,12 +263,15 @@ def set_3d_properties(self, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ xs = self.get_xdata() ys = self.get_ydata() zs = cbook._to_unmasked_float_array(zs).ravel() zs = np.broadcast_to(zs, len(xs)) self._verts3d = juggle_axes(xs, ys, zs, zdir) + self._axlim_clip = axlim_clip self.stale = True def set_data_3d(self, *args): @@ -266,14 +312,19 @@ def get_data_3d(self): @artist.allow_rasterization def draw(self, renderer): - xs3d, ys3d, zs3d = self._verts3d - xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) + if self._axlim_clip: + xs3d, ys3d, zs3d = _viewlim_mask(*self._verts3d, self.axes) + else: + xs3d, ys3d, zs3d = self._verts3d + xs, ys, zs, tis = proj3d._proj_transform_clip(xs3d, ys3d, zs3d, + self.axes.M, + self.axes._focal_length) self.set_data(xs, ys) super().draw(renderer) self.stale = False -def line_2d_to_3d(line, zs=0, zdir='z'): +def line_2d_to_3d(line, zs=0, zdir='z', axlim_clip=False): """ Convert a `.Line2D` to a `.Line3D` object. @@ -284,10 +335,12 @@ def line_2d_to_3d(line, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ line.__class__ = Line3D - line.set_3d_properties(zs, zdir) + line.set_3d_properties(zs, zdir, axlim_clip) def _path_to_3d_segment(path, zs=0, zdir='z'): @@ -349,15 +402,18 @@ class Collection3D(Collection): def do_3d_projection(self): """Project the points according to renderer matrix.""" - xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) - for vs, _ in self._3dverts_codes] - self._paths = [mpath.Path(np.column_stack([xs, ys]), cs) + vs_list = [vs for vs, _ in self._3dverts_codes] + if self._axlim_clip: + vs_list = [np.ma.row_stack(_viewlim_mask(*vs.T, self.axes)).T + for vs in vs_list] + xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) for vs in vs_list] + self._paths = [mpath.Path(np.ma.column_stack([xs, ys]), cs) for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)] zs = np.concatenate([zs for _, _, zs in xyzs_list]) return zs.min() if len(zs) else 1e9 -def collection_2d_to_3d(col, zs=0, zdir='z'): +def collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.Collection` to a `.Collection3D` object.""" zs = np.broadcast_to(zs, len(col.get_paths())) col._3dverts_codes = [ @@ -367,12 +423,16 @@ def collection_2d_to_3d(col, zs=0, zdir='z'): p.codes) for p, z in zip(col.get_paths(), zs)] col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col)) + col._axlim_clip = axlim_clip class Line3DCollection(LineCollection): """ A collection of 3D lines. """ + def __init__(self, lines, axlim_clip=False, **kwargs): + super().__init__(lines, **kwargs) + self._axlim_clip = axlim_clip def set_sort_zpos(self, val): """Set the position to use for z-sorting.""" @@ -390,9 +450,16 @@ def do_3d_projection(self): """ Project the points according to renderer matrix. """ + segments = self._segments3d + if self._axlim_clip: + all_points = np.ma.vstack(segments) + masked_points = np.ma.column_stack([*_viewlim_mask(*all_points.T, + self.axes)]) + segment_lengths = [np.shape(segment)[0] for segment in segments] + segments = np.split(masked_points, np.cumsum(segment_lengths[:-1])) xyslist = [proj3d._proj_trans_points(points, self.axes.M) - for points in self._segments3d] - segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist] + for points in segments] + segments_2d = [np.ma.column_stack([xs, ys]) for xs, ys, zs in xyslist] LineCollection.set_segments(self, segments_2d) # FIXME @@ -402,11 +469,12 @@ def do_3d_projection(self): return minz -def line_collection_2d_to_3d(col, zs=0, zdir='z'): +def line_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.LineCollection` to a `.Line3DCollection` object.""" segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir) col.__class__ = Line3DCollection col.set_segments(segments3d) + col._axlim_clip = axlim_clip class Patch3D(Patch): @@ -414,7 +482,7 @@ class Patch3D(Patch): 3D patch object. """ - def __init__(self, *args, zs=(), zdir='z', **kwargs): + def __init__(self, *args, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -425,11 +493,13 @@ def __init__(self, *args, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) - def set_3d_properties(self, verts, zs=0, zdir='z'): + def set_3d_properties(self, verts, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the patch. @@ -442,10 +512,13 @@ def set_3d_properties(self, verts, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ zs = np.broadcast_to(zs, len(verts)) self._segment3d = [juggle_axes(x, y, z, zdir) for ((x, y), z) in zip(verts, zs)] + self._axlim_clip = axlim_clip def get_path(self): # docstring inherited @@ -457,10 +530,14 @@ def get_path(self): def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) - self._path2d = mpath.Path(np.column_stack([vxs, vys])) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys])) return min(vzs) @@ -469,7 +546,7 @@ class PathPatch3D(Patch3D): 3D PathPatch object. """ - def __init__(self, path, *, zs=(), zdir='z', **kwargs): + def __init__(self, path, *, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -480,12 +557,14 @@ def __init__(self, path, *, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ # Not super().__init__! Patch.__init__(self, **kwargs) - self.set_3d_properties(path, zs, zdir) + self.set_3d_properties(path, zs, zdir, axlim_clip) - def set_3d_properties(self, path, zs=0, zdir='z'): + def set_3d_properties(self, path, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the path patch. @@ -498,16 +577,23 @@ def set_3d_properties(self, path, zs=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ - Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) + Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) self._code3d = path.codes def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) - self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]), self._code3d) return min(vzs) @@ -519,11 +605,11 @@ def _get_patch_verts(patch): return polygons[0] if len(polygons) else np.array([]) -def patch_2d_to_3d(patch, z=0, zdir='z'): +def patch_2d_to_3d(patch, z=0, zdir='z', axlim_clip=False): """Convert a `.Patch` to a `.Patch3D` object.""" verts = _get_patch_verts(patch) patch.__class__ = Patch3D - patch.set_3d_properties(verts, z, zdir) + patch.set_3d_properties(verts, z, zdir, axlim_clip) def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): @@ -541,7 +627,8 @@ class Patch3DCollection(PatchCollection): A collection of 3D patches. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D patches with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -558,7 +645,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): """ self._depthshade = depthshade super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) def get_depthshade(self): return self._depthshade @@ -581,7 +668,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the patches. @@ -594,6 +681,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot patches orthogonal to. All patches must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -607,14 +696,19 @@ def set_3d_properties(self, zs, zdir): self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) self._z_markers_idx = slice(-1) self._vzs = None + self._axlim_clip = axlim_clip self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) self._vzs = vzs - super().set_offsets(np.column_stack([vxs, vys])) + super().set_offsets(np.ma.column_stack([vxs, vys])) if vzs.size > 0: return min(vzs) @@ -648,7 +742,8 @@ class Path3DCollection(PathCollection): A collection of 3D paths. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D paths with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -666,7 +761,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): self._depthshade = depthshade self._in_draw = False super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) self._offset_zordered = None def draw(self, renderer): @@ -679,7 +774,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the paths. @@ -692,6 +787,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot paths orthogonal to. All paths must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide paths with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -702,6 +799,7 @@ def set_3d_properties(self, zs, zdir): else: xs = [] ys = [] + self._zdir = zdir self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) # In the base draw methods we access the attributes directly which # means we cannot resolve the shuffling in the getter methods like @@ -722,6 +820,8 @@ def set_3d_properties(self, zs, zdir): # points and point properties according to the index array self._z_markers_idx = slice(-1) self._vzs = None + + self._axlim_clip = axlim_clip self.stale = True def set_sizes(self, sizes, dpi=72.0): @@ -751,13 +851,17 @@ def set_depthshade(self, depthshade): self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) # Sort the points based on z coordinates # Performance optimization: Create a sorted index array and reorder # points and point properties according to the index array - z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1] + z_markers_idx = self._z_markers_idx = np.ma.argsort(vzs)[::-1] self._vzs = vzs # we have to special case the sizes because of code in collections.py @@ -771,7 +875,7 @@ def do_3d_projection(self): if len(self._linewidths3d) > 1: self._linewidths = self._linewidths3d[z_markers_idx] - PathCollection.set_offsets(self, np.column_stack((vxs, vys))) + PathCollection.set_offsets(self, np.ma.column_stack((vxs, vys))) # Re-order items vzs = vzs[z_markers_idx] @@ -779,7 +883,7 @@ def do_3d_projection(self): vys = vys[z_markers_idx] # Store ordered offset for drawing purpose - self._offset_zordered = np.column_stack((vxs, vys)) + self._offset_zordered = np.ma.column_stack((vxs, vys)) return np.min(vzs) if vzs.size else np.nan @@ -819,7 +923,7 @@ def get_edgecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) -def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): +def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True, axlim_clip=False): """ Convert a `.PatchCollection` into a `.Patch3DCollection` object (or a `.PathCollection` into a `.Path3DCollection` object). @@ -837,7 +941,8 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): See `.get_dir_vector` for a description of the values. depthshade : bool, default: True Whether to shade the patches to give a sense of depth. - + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ if isinstance(col, PathCollection): col.__class__ = Path3DCollection @@ -846,7 +951,7 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): col.__class__ = Patch3DCollection col._depthshade = depthshade col._in_draw = False - col.set_3d_properties(zs, zdir) + col.set_3d_properties(zs, zdir, axlim_clip) class Poly3DCollection(PolyCollection): @@ -871,7 +976,7 @@ class Poly3DCollection(PolyCollection): """ def __init__(self, verts, *args, zsort='average', shade=False, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Parameters ---------- @@ -893,6 +998,9 @@ def __init__(self, verts, *args, zsort='average', shade=False, .. versionadded:: 3.7 + axlim_clip : bool, default: False + Whether to hide polygons with a vertex outside the view limits. + *args, **kwargs All other parameters are forwarded to `.PolyCollection`. @@ -927,6 +1035,7 @@ def __init__(self, verts, *args, zsort='average', shade=False, raise ValueError('verts must be a list of (N, 3) array-like') self.set_zsort(zsort) self._codes3d = None + self._axlim_clip = axlim_clip _zsort_functions = { 'average': np.average, @@ -948,7 +1057,11 @@ def set_zsort(self, zsort): self._sort_zpos = None self.stale = True + @_api.deprecated("3.10") def get_vector(self, segments3d): + return self._get_vector(segments3d) + + def _get_vector(self, segments3d): """Optimize points for projection.""" if len(segments3d): xs, ys, zs = np.vstack(segments3d).T @@ -974,7 +1087,7 @@ def set_verts(self, verts, closed=True): Whether the polygon should be closed by adding a CLOSEPOLY connection at the end. """ - self.get_vector(verts) + self._get_vector(verts) # 2D verts will be updated at draw time super().set_verts([], False) self._closed = closed @@ -987,7 +1100,7 @@ def set_verts_and_codes(self, verts, codes): # and set our own codes instead. self._codes3d = codes - def set_3d_properties(self): + def set_3d_properties(self, axlim_clip=False): # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. self.update_scalarmappable() @@ -1020,7 +1133,16 @@ def do_3d_projection(self): self._facecolor3d = self._facecolors if self._edge_is_mapped: self._edgecolor3d = self._edgecolors - txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._vec[0:3], self.axes) + if self._vec.shape[0] == 4: # Will be 3 (xyz) or 4 (xyzw) + w_masked = np.ma.masked_where(zs.mask, self._vec[3]) + vec = np.ma.array([xs, ys, zs, w_masked]) + else: + vec = np.ma.array([xs, ys, zs]) + else: + vec = self._vec + txs, tys, tzs = proj3d._proj_transform_vec(vec, self.axes.M) xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] # This extra fuss is to re-order face / edge colors @@ -1037,7 +1159,7 @@ def do_3d_projection(self): if xyzlist: # sort by depth (furthest drawn first) z_segments_2d = sorted( - ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) + ((self._zsortfunc(zs.data), np.ma.column_stack([xs, ys]), fc, ec, idx) for idx, ((xs, ys, zs), fc, ec) in enumerate(zip(xyzlist, cface, cedge))), key=lambda x: x[0], reverse=True) @@ -1114,7 +1236,7 @@ def get_edgecolor(self): return np.asarray(self._edgecolors2d) -def poly_collection_2d_to_3d(col, zs=0, zdir='z'): +def poly_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """ Convert a `.PolyCollection` into a `.Poly3DCollection` object. @@ -1134,6 +1256,7 @@ def poly_collection_2d_to_3d(col, zs=0, zdir='z'): col.__class__ = Poly3DCollection col.set_verts_and_codes(segments_3d, codes) col.set_3d_properties() + col._axlim_clip = axlim_clip def juggle_axes(xs, ys, zs, zdir): @@ -1181,6 +1304,47 @@ def _zalpha(colors, zs): return np.column_stack([rgba[:, :3], rgba[:, 3] * sats]) +def _all_points_on_plane(xs, ys, zs, atol=1e-8): + """ + Check if all points are on the same plane. Note that NaN values are + ignored. + + Parameters + ---------- + xs, ys, zs : array-like + The x, y, and z coordinates of the points. + atol : float, default: 1e-8 + The tolerance for the equality check. + """ + xs, ys, zs = np.asarray(xs), np.asarray(ys), np.asarray(zs) + points = np.column_stack([xs, ys, zs]) + points = points[~np.isnan(points).any(axis=1)] + # Check for the case where we have less than 3 unique points + points = np.unique(points, axis=0) + if len(points) <= 3: + return True + # Calculate the vectors from the first point to all other points + vs = (points - points[0])[1:] + vs = vs / np.linalg.norm(vs, axis=1)[:, np.newaxis] + # Filter out parallel vectors + vs = np.unique(vs, axis=0) + if len(vs) <= 2: + return True + # Filter out parallel and antiparallel vectors to the first vector + cross_norms = np.linalg.norm(np.cross(vs[0], vs[1:]), axis=1) + zero_cross_norms = np.where(np.isclose(cross_norms, 0, atol=atol))[0] + 1 + vs = np.delete(vs, zero_cross_norms, axis=0) + if len(vs) <= 2: + return True + # Calculate the normal vector from the first three points + n = np.cross(vs[0], vs[1]) + n = n / np.linalg.norm(n) + # If the dot product of the normal vector and all other vectors is zero, + # all points are on the same plane + dots = np.dot(n, vs.transpose()) + return np.allclose(dots, 0, atol=atol) + + def _generate_normals(polygons): """ Compute the normals of a list of polygons, one normal per polygon. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 71cd8f062d40..d0ba360c314b 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -14,6 +14,7 @@ import itertools import math import textwrap +import warnings import numpy as np @@ -57,11 +58,13 @@ class Axes3D(Axes): Axes._shared_axes["view"] = cbook.Grouper() def __init__( - self, fig, rect=None, *args, - elev=30, azim=-60, roll=0, sharez=None, proj_type='persp', - box_aspect=None, computed_zorder=True, focal_length=None, - shareview=None, - **kwargs): + self, fig, rect=None, *args, + elev=30, azim=-60, roll=0, shareview=None, sharez=None, + proj_type='persp', focal_length=None, + box_aspect=None, + computed_zorder=True, + **kwargs, + ): """ Parameters ---------- @@ -82,11 +85,21 @@ def __init__( The roll angle in degrees rotates the camera about the viewing axis. A positive angle spins the camera clockwise, causing the scene to rotate counter-clockwise. + shareview : Axes3D, optional + Other Axes to share view angles with. Note that it is not possible + to unshare axes. sharez : Axes3D, optional Other Axes to share z-limits with. Note that it is not possible to unshare axes. proj_type : {'persp', 'ortho'} The projection type, default 'persp'. + focal_length : float, default: None + For a projection type of 'persp', the focal length of the virtual + camera. Must be > 0. If None, defaults to 1. + For a projection type of 'ortho', must be set to either None + or infinity (numpy.inf). If None, defaults to infinity. + The focal length can be computed from a desired Field Of View via + the equation: focal_length = 1/tan(FOV/2) box_aspect : 3-tuple of floats, default: None Changes the physical dimensions of the Axes3D, such that the ratio of the axis lengths in display units is x:y:z. @@ -100,16 +113,6 @@ def __init__( does not produce the desired result. Note however, that a manual zorder will only be correct for a limited view angle. If the figure is rotated by the user, it will look wrong from certain angles. - focal_length : float, default: None - For a projection type of 'persp', the focal length of the virtual - camera. Must be > 0. If None, defaults to 1. - For a projection type of 'ortho', must be set to either None - or infinity (numpy.inf). If None, defaults to infinity. - The focal length can be computed from a desired Field Of View via - the equation: focal_length = 1/tan(FOV/2) - shareview : Axes3D, optional - Other Axes to share view angles with. Note that it is not possible - to unshare axes. **kwargs Other optional keyword arguments: @@ -170,11 +173,12 @@ def __init__( self.fmt_zdata = None self.mouse_init() - self.figure.canvas.callbacks._connect_picklable( + fig = self.get_figure(root=True) + fig.canvas.callbacks._connect_picklable( 'motion_notify_event', self._on_move) - self.figure.canvas.callbacks._connect_picklable( + fig.canvas.callbacks._connect_picklable( 'button_press_event', self._button_press) - self.figure.canvas.callbacks._connect_picklable( + fig.canvas.callbacks._connect_picklable( 'button_release_event', self._button_release) self.set_top_view() @@ -1361,17 +1365,21 @@ def _button_press(self, event): if event.inaxes == self: self.button_pressed = event.button self._sx, self._sy = event.xdata, event.ydata - toolbar = self.figure.canvas.toolbar + toolbar = self.get_figure(root=True).canvas.toolbar if toolbar and toolbar._nav_stack() is None: toolbar.push_current() + if toolbar: + toolbar.set_message(toolbar._mouse_event_to_message(event)) def _button_release(self, event): self.button_pressed = None - toolbar = self.figure.canvas.toolbar + toolbar = self.get_figure(root=True).canvas.toolbar # backend_bases.release_zoom and backend_bases.release_pan call # push_current, so check the navigation mode so we don't call it twice if toolbar and self.get_navigate_mode() is None: toolbar.push_current() + if toolbar: + toolbar.set_message(toolbar._mouse_event_to_message(event)) def _get_view(self): # docstring inherited @@ -1500,6 +1508,39 @@ def _calc_coord(self, xv, yv, renderer=None): p2 = p1 - scale*vec return p2, pane_idx + def _arcball(self, x: float, y: float) -> np.ndarray: + """ + Convert a point (x, y) to a point on a virtual trackball. + + This is Ken Shoemake's arcball (a sphere), modified + to soften the abrupt edge (optionally). + See: Ken Shoemake, "ARCBALL: A user interface for specifying + three-dimensional rotation using a mouse." in + Proceedings of Graphics Interface '92, 1992, pp. 151-156, + https://doi.org/10.20380/GI1992.18 + The smoothing of the edge is inspired by Gavin Bell's arcball + (a sphere combined with a hyperbola), but here, the sphere + is combined with a section of a cylinder, so it has finite support. + """ + s = mpl.rcParams['axes3d.trackballsize'] / 2 + b = mpl.rcParams['axes3d.trackballborder'] / s + x /= s + y /= s + r2 = x*x + y*y + r = np.sqrt(r2) + ra = 1 + b + a = b * (1 + b/2) + ri = 2/(ra + 1/ra) + if r < ri: + p = np.array([np.sqrt(1 - r2), x, y]) + elif r < ra: + dr = ra - r + p = np.array([a - np.sqrt((a + dr) * (a - dr)), x, y]) + p /= np.linalg.norm(p) + else: + p = np.array([0, x/r, y/r]) + return p + def _on_move(self, event): """ Mouse moving. @@ -1535,12 +1576,35 @@ def _on_move(self, event): if dx == 0 and dy == 0: return - roll = np.deg2rad(self.roll) - delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll) - dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) - elev = self.elev + delev - azim = self.azim + dazim - roll = self.roll + style = mpl.rcParams['axes3d.mouserotationstyle'] + if style == 'azel': + roll = np.deg2rad(self.roll) + delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll) + dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) + elev = self.elev + delev + azim = self.azim + dazim + roll = self.roll + else: + q = _Quaternion.from_cardan_angles( + *np.deg2rad((self.elev, self.azim, self.roll))) + + if style == 'trackball': + k = np.array([0, -dy/h, dx/w]) + nk = np.linalg.norm(k) + th = nk / mpl.rcParams['axes3d.trackballsize'] + dq = _Quaternion(np.cos(th), k*np.sin(th)/nk) + else: # 'sphere', 'arcball' + current_vec = self._arcball(self._sx/w, self._sy/h) + new_vec = self._arcball(x/w, y/h) + if style == 'sphere': + dq = _Quaternion.rotate_from_to(current_vec, new_vec) + else: # 'arcball' + dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec) + + q = dq * q + elev, azim, roll = np.rad2deg(q.as_cardan_angles()) + + # update view vertical_axis = self._axis_names[self._vertical_axis] self.view_init( elev=elev, @@ -1569,7 +1633,7 @@ def _on_move(self, event): # Store the event coordinates for the next time through. self._sx, self._sy = x, y # Always request a draw update at the end of interaction - self.figure.canvas.draw_idle() + self.get_figure(root=True).canvas.draw_idle() def drag_pan(self, button, key, x, y): # docstring inherited @@ -1764,7 +1828,7 @@ def get_zlabel(self): """ Get the z-label text string. """ - label = self.zaxis.get_label() + label = self.zaxis.label return label.get_text() # Axes rectangle characteristics @@ -1851,7 +1915,7 @@ def get_zbound(self): else: return upper, lower - def text(self, x, y, z, s, zdir=None, **kwargs): + def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs): """ Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates. @@ -1864,6 +1928,10 @@ def text(self, x, y, z, s, zdir=None, **kwargs): zdir : {'x', 'y', 'z', 3-tuple}, optional The direction to be used as the z-direction. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.text`. @@ -1873,13 +1941,13 @@ def text(self, x, y, z, s, zdir=None, **kwargs): The created `.Text3D` instance. """ text = super().text(x, y, s, **kwargs) - art3d.text_2d_to_3d(text, z, zdir) + art3d.text_2d_to_3d(text, z, zdir, axlim_clip) return text text3D = text text2D = Axes.text - def plot(self, xs, ys, *args, zdir='z', **kwargs): + def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs): """ Plot 2D or 3D data. @@ -1894,6 +1962,10 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): each point. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z. + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ @@ -1913,7 +1985,7 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): lines = super().plot(xs, ys, *args, **kwargs) for line in lines: - art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) + art3d.line_2d_to_3d(line, zs=zs, zdir=zdir, axlim_clip=axlim_clip) xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) self.auto_scale_xyz(xs, ys, zs, had_data) @@ -1921,8 +1993,137 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): plot3D = plot + def fill_between(self, x1, y1, z1, x2, y2, z2, *, + where=None, mode='auto', facecolors=None, shade=None, + axlim_clip=False, **kwargs): + """ + Fill the area between two 3D curves. + + The curves are defined by the points (*x1*, *y1*, *z1*) and + (*x2*, *y2*, *z2*). This creates one or multiple quadrangle + polygons that are filled. All points must be the same length N, or a + single value to be used for all points. + + Parameters + ---------- + x1, y1, z1 : float or 1D array-like + x, y, and z coordinates of vertices for 1st line. + + x2, y2, z2 : float or 1D array-like + x, y, and z coordinates of vertices for 2nd line. + + where : array of bool (length N), optional + Define *where* to exclude some regions from being filled. The + filled regions are defined by the coordinates ``pts[where]``, + for all x, y, and z pts. More precisely, fill between ``pts[i]`` + and ``pts[i+1]`` if ``where[i] and where[i+1]``. Note that this + definition implies that an isolated *True* value between two + *False* values in *where* will not result in filling. Both sides of + the *True* position remain unfilled due to the adjacent *False* + values. + + mode : {'quad', 'polygon', 'auto'}, default: 'auto' + The fill mode. One of: + + - 'quad': A separate quadrilateral polygon is created for each + pair of subsequent points in the two lines. + - 'polygon': The two lines are connected to form a single polygon. + This is faster and can render more cleanly for simple shapes + (e.g. for filling between two lines that lie within a plane). + - '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 + Colors of each individual patch, or a single color to be used for + all patches. + + shade : bool, default: None + Whether to shade the facecolors. If *None*, then defaults to *True* + for 'quad' mode and *False* for 'polygon' mode. + + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. + + .. versionadded:: 3.10 + + **kwargs + All other keyword arguments are passed on to `.Poly3DCollection`. + + Returns + ------- + `.Poly3DCollection` + A `.Poly3DCollection` containing the plotted polygons. + + """ + _api.check_in_list(['auto', 'quad', 'polygon'], mode=mode) + + had_data = self.has_data() + x1, y1, z1, x2, y2, z2 = cbook._broadcast_with_masks(x1, y1, z1, x2, y2, z2) + + if facecolors is None: + facecolors = [self._get_patches_for_fill.get_next_color()] + facecolors = list(mcolors.to_rgba_array(facecolors)) + + if where is None: + where = True + else: + where = np.asarray(where, dtype=bool) + if where.size != x1.size: + raise ValueError(f"where size ({where.size}) does not match " + f"size ({x1.size})") + where = where & ~np.isnan(x1) # NaNs were broadcast in _broadcast_with_masks + + if mode == 'auto': + if art3d._all_points_on_plane(np.concatenate((x1[where], x2[where])), + np.concatenate((y1[where], y2[where])), + np.concatenate((z1[where], z2[where])), + atol=1e-12): + mode = 'polygon' + else: + mode = 'quad' + + if shade is None: + if mode == 'quad': + shade = True + else: + shade = False + + polys = [] + for idx0, idx1 in cbook.contiguous_regions(where): + x1i = x1[idx0:idx1] + y1i = y1[idx0:idx1] + z1i = z1[idx0:idx1] + x2i = x2[idx0:idx1] + y2i = y2[idx0:idx1] + z2i = z2[idx0:idx1] + + if not len(x1i): + continue + + if mode == 'quad': + # Preallocate the array for the region's vertices, and fill it in + n_polys_i = len(x1i) - 1 + polys_i = np.empty((n_polys_i, 4, 3)) + polys_i[:, 0, :] = np.column_stack((x1i[:-1], y1i[:-1], z1i[:-1])) + polys_i[:, 1, :] = np.column_stack((x1i[1:], y1i[1:], z1i[1:])) + polys_i[:, 2, :] = np.column_stack((x2i[1:], y2i[1:], z2i[1:])) + polys_i[:, 3, :] = np.column_stack((x2i[:-1], y2i[:-1], z2i[:-1])) + polys = polys + [*polys_i] + elif mode == 'polygon': + line1 = np.column_stack((x1i, y1i, z1i)) + line2 = np.column_stack((x2i[::-1], y2i[::-1], z2i[::-1])) + poly = np.concatenate((line1, line2), axis=0) + polys.append(poly) + + polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade, + axlim_clip=axlim_clip, **kwargs) + self.add_collection(polyc) + + self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data) + return polyc + def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, - vmax=None, lightsource=None, **kwargs): + vmax=None, lightsource=None, axlim_clip=False, **kwargs): """ Create a surface plot. @@ -1987,6 +2188,11 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. + + .. versionadded:: 3.10 + **kwargs Other keyword arguments are forwarded to `.Poly3DCollection`. """ @@ -2046,8 +2252,8 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, col_inds = list(range(0, cols-1, cstride)) + [cols-1] polys = [] - for rs, rs_next in zip(row_inds[:-1], row_inds[1:]): - for cs, cs_next in zip(col_inds[:-1], col_inds[1:]): + for rs, rs_next in itertools.pairwise(row_inds): + for cs, cs_next in itertools.pairwise(col_inds): ps = [ # +1 ensures we share edges between polygons cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1]) @@ -2087,9 +2293,9 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, if fcolors is not None: polyc = art3d.Poly3DCollection( polys, edgecolors=colset, facecolors=colset, shade=shade, - lightsource=lightsource, **kwargs) + lightsource=lightsource, axlim_clip=axlim_clip, **kwargs) elif cmap: - polyc = art3d.Poly3DCollection(polys, **kwargs) + polyc = art3d.Poly3DCollection(polys, axlim_clip=axlim_clip, **kwargs) # can't always vectorize, because polys might be jagged if isinstance(polys, np.ndarray): avg_z = polys[..., 2].mean(axis=-1) @@ -2107,15 +2313,15 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, color = np.array(mcolors.to_rgba(color)) polyc = art3d.Poly3DCollection( - polys, facecolors=color, shade=shade, - lightsource=lightsource, **kwargs) + polys, facecolors=color, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(X, Y, Z, had_data) return polyc - def plot_wireframe(self, X, Y, Z, **kwargs): + def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs): """ Plot a 3D wireframe. @@ -2131,6 +2337,12 @@ def plot_wireframe(self, X, Y, Z, **kwargs): X, Y, Z : 2D arrays Data values. + axlim_clip : bool, default: False + Whether to hide lines and patches with vertices outside the axes + view limits. + + .. versionadded:: 3.10 + rcount, ccount : int Maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these @@ -2227,14 +2439,14 @@ def plot_wireframe(self, X, Y, Z, **kwargs): + [list(zip(xl, yl, zl)) for xl, yl, zl in zip(txlines, tylines, tzlines)]) - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(X, Y, Z, had_data) return linec def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Plot a triangulated surface. @@ -2276,6 +2488,10 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, *cmap* is specified. lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. + + .. versionadded:: 3.10 **kwargs All other keyword arguments are passed on to :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` @@ -2312,7 +2528,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, verts = np.stack((xt, yt, zt), axis=-1) if cmap: - polyc = art3d.Poly3DCollection(verts, *args, **kwargs) + polyc = art3d.Poly3DCollection(verts, *args, + axlim_clip=axlim_clip, **kwargs) # average over the three points of each triangle avg_z = verts[:, :, 2].mean(axis=1) polyc.set_array(avg_z) @@ -2323,7 +2540,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, else: polyc = art3d.Poly3DCollection( verts, *args, shade=shade, lightsource=lightsource, - facecolors=color, **kwargs) + facecolors=color, axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(tri.x, tri.y, z, had_data) @@ -2359,18 +2576,21 @@ def _3d_extend_contour(self, cset, stride=5): cset.remove() def add_contour_set( - self, cset, extend3d=False, stride=5, zdir='z', offset=None): + self, cset, extend3d=False, stride=5, zdir='z', offset=None, + axlim_clip=False): zdir = '-' + zdir if extend3d: self._3d_extend_contour(cset, stride) else: art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else cset.levels, zdir=zdir) + cset, zs=offset if offset is not None else cset.levels, zdir=zdir, + axlim_clip=axlim_clip) - def add_contourf_set(self, cset, zdir='z', offset=None): - self._add_contourf_set(cset, zdir=zdir, offset=offset) + def add_contourf_set(self, cset, zdir='z', offset=None, *, axlim_clip=False): + self._add_contourf_set(cset, zdir=zdir, offset=offset, + axlim_clip=axlim_clip) - def _add_contourf_set(self, cset, zdir='z', offset=None): + def _add_contourf_set(self, cset, zdir='z', offset=None, axlim_clip=False): """ Returns ------- @@ -2389,12 +2609,14 @@ def _add_contourf_set(self, cset, zdir='z', offset=None): midpoints = np.append(midpoints, max_level) art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else midpoints, zdir=zdir) + cset, zs=offset if offset is not None else midpoints, zdir=zdir, + axlim_clip=axlim_clip) return midpoints @_preprocess_data() def contour(self, X, Y, Z, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2411,6 +2633,10 @@ def contour(self, X, Y, Z, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -2425,7 +2651,7 @@ def contour(self, X, Y, Z, *args, jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contour(jX, jY, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2434,7 +2660,8 @@ def contour(self, X, Y, Z, *args, @_preprocess_data() def tricontour(self, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2455,6 +2682,10 @@ def tricontour(self, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2480,7 +2711,7 @@ def tricontour(self, *args, tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontour(tri, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2496,7 +2727,8 @@ def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data): self.auto_scale_xyz(*limits, had_data) @_preprocess_data() - def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): + def contourf(self, X, Y, Z, *args, + zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2509,6 +2741,10 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2522,7 +2758,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contourf(jX, jY, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset @@ -2530,7 +2766,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): contourf3D = contourf @_preprocess_data() - def tricontourf(self, *args, zdir='z', offset=None, **kwargs): + def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2547,6 +2783,10 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to zdir. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2573,12 +2813,13 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontourf(tri, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset - def add_collection3d(self, col, zs=0, zdir='z', autolim=True): + def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, + axlim_clip=False): """ Add a 3D collection object to the plot. @@ -2602,6 +2843,10 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): The direction to use for the z-positions. autolim : bool, default: True Whether to update the data limits. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 """ had_data = self.has_data() @@ -2613,13 +2858,16 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): # object would also pass.) Maybe have a collection3d # abstract class to test for and exclude? if type(col) is mcoll.PolyCollection: - art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.LineCollection: - art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.PatchCollection: - art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) if autolim: @@ -2640,8 +2888,9 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): @_preprocess_data(replace_names=["xs", "ys", "zs", "s", "edgecolors", "c", "facecolor", "facecolors", "color"]) - def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, - *args, **kwargs): + def scatter(self, xs, ys, + zs=0, zdir='z', s=20, c=None, depthshade=True, *args, + axlim_clip=False, **kwargs): """ Create a scatter plot. @@ -2677,6 +2926,10 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2705,7 +2958,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs) art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, - depthshade=depthshade) + depthshade=depthshade, + axlim_clip=axlim_clip) if self._zmargin < 0.05 and xs.size > 0: self.set_zmargin(0.05) @@ -2717,7 +2971,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, scatter3D = scatter @_preprocess_data() - def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): + def bar(self, left, height, zs=0, zdir='z', *args, + axlim_clip=False, **kwargs): """ Add 2D bar(s). @@ -2732,6 +2987,10 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): used for all bars. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). + axlim_clip : bool, default: False + Whether to hide bars with points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2754,7 +3013,7 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): vs = art3d._get_patch_verts(p) verts += vs.tolist() verts_zs += [z] * len(vs) - art3d.patch_2d_to_3d(p, z, zdir) + art3d.patch_2d_to_3d(p, z, zdir, axlim_clip) if 'alpha' in kwargs: p.set_alpha(kwargs['alpha']) @@ -2773,7 +3032,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): @_preprocess_data() def bar3d(self, x, y, z, dx, dy, dz, color=None, - zsort='average', shade=True, lightsource=None, *args, **kwargs): + zsort='average', shade=True, lightsource=None, *args, + axlim_clip=False, **kwargs): """ Generate a 3D barplot. @@ -2820,6 +3080,11 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide the bars with points outside the axes view limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -2925,6 +3190,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, facecolors=facecolors, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, *args, **kwargs) self.add_collection(col) @@ -2942,7 +3208,7 @@ def set_title(self, label, fontdict=None, loc='center', **kwargs): @_preprocess_data() def quiver(self, X, Y, Z, U, V, W, *, length=1, arrow_length_ratio=.3, pivot='tail', normalize=False, - **kwargs): + axlim_clip=False, **kwargs): """ Plot a 3D field of arrows. @@ -2974,6 +3240,11 @@ def quiver(self, X, Y, Z, U, V, W, *, Whether all arrows are normalized to have the same length, or keep the lengths defined by *u*, *v*, and *w*. + axlim_clip : bool, default: False + Whether to hide arrows with points outside the axes view limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3055,7 +3326,7 @@ def calc_arrows(UVW): else: lines = [] - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) @@ -3065,7 +3336,7 @@ def calc_arrows(UVW): quiver3D = quiver def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \ **kwargs) @@ -3112,6 +3383,11 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide voxels with points outside the axes view limits. + + .. versionadded:: 3.10 + **kwargs Additional keyword arguments to pass onto `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. @@ -3225,7 +3501,7 @@ def permutation_matrices(n): voxel_faces[i0].append(p0 + square_rot_neg) # draw middle faces - for r1, r2 in zip(rinds[:-1], rinds[1:]): + for r1, r2 in itertools.pairwise(rinds): p1 = permute.dot([p, q, r1]) p2 = permute.dot([p, q, r2]) @@ -3267,7 +3543,8 @@ def permutation_matrices(n): poly = art3d.Poly3DCollection( faces, facecolors=facecolor, edgecolors=edgecolor, - shade=shade, lightsource=lightsource, **kwargs) + shade=shade, lightsource=lightsource, axlim_clip=axlim_clip, + **kwargs) self.add_collection3d(poly) polygons[coord] = poly @@ -3278,6 +3555,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', barsabove=False, errorevery=1, ecolor=None, elinewidth=None, capsize=None, capthick=None, xlolims=False, xuplims=False, ylolims=False, yuplims=False, zlolims=False, zuplims=False, + axlim_clip=False, **kwargs): """ Plot lines and/or markers with errorbars around them. @@ -3355,6 +3633,11 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', Used to avoid overlapping error bars when two series share x-axis values. + axlim_clip : bool, default: False + Whether to hide error bars that are outside the axes limits. + + .. versionadded:: 3.10 + Returns ------- errlines : list @@ -3410,7 +3693,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', # data processing. (data_line, base_style), = self._get_lines._plot_args( self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) - art3d.line_2d_to_3d(data_line, zs=z) + art3d.line_2d_to_3d(data_line, zs=z, axlim_clip=axlim_clip) # Do this after creating `data_line` to avoid modifying `base_style`. if barsabove: @@ -3496,7 +3779,7 @@ def _extract_errs(err, data, lomask, himask): # them directly in planar form. quiversize = eb_cap_style.get('markersize', mpl.rcParams['lines.markersize']) ** 2 - quiversize *= self.figure.dpi / 72 + quiversize *= self.get_figure(root=True).dpi / 72 quiversize = self.transAxes.inverted().transform([ (0, 0), (quiversize, quiversize)]) quiversize = np.mean(np.diff(quiversize, axis=0)) @@ -3554,9 +3837,11 @@ def _extract_errs(err, data, lomask, himask): # these markers will rotate as the viewing angle changes cap_lo = art3d.Line3D(*lo_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) cap_hi = art3d.Line3D(*hi_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) self.add_line(cap_lo) self.add_line(cap_hi) @@ -3571,6 +3856,7 @@ def _extract_errs(err, data, lomask, himask): self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style) errline = art3d.Line3DCollection(np.array(coorderr).T, + axlim_clip=axlim_clip, **eb_lines_style) self.add_collection(errline) errlines.append(errline) @@ -3597,9 +3883,8 @@ def _digout_minmax(err_arr, coord_label): return errlines, caplines, limmarks - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, - bbox_extra_artists=None, *, for_layout_only=False): + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, + bbox_extra_artists=None, for_layout_only=False): ret = super().get_tightbbox(renderer, call_axes_locator=call_axes_locator, bbox_extra_artists=bbox_extra_artists, @@ -3616,7 +3901,7 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True, @_preprocess_data() def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', - bottom=0, label=None, orientation='z'): + bottom=0, label=None, orientation='z', axlim_clip=False): """ Create a 3D stem plot. @@ -3666,6 +3951,11 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', orientation : {'x', 'y', 'z'}, default: 'z' The direction along which stems are drawn. + axlim_clip : bool, default: False + Whether to hide stems that are outside the axes limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3717,7 +4007,8 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', baseline, = self.plot(basex, basey, basefmt, zs=bottom, zdir=orientation, label='_nolegend_') stemlines = art3d.Line3DCollection( - lines, linestyles=linestyle, colors=linecolor, label='_nolegend_') + lines, linestyles=linestyle, colors=linecolor, label='_nolegend_', + axlim_clip=axlim_clip) self.add_collection(stemlines) markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') @@ -3748,3 +4039,124 @@ def get_test_data(delta=0.05): Y = Y * 10 Z = Z * 500 return X, Y, Z + + +class _Quaternion: + """ + Quaternions + consisting of scalar, along 1, and vector, with components along i, j, k + """ + + def __init__(self, scalar, vector): + self.scalar = scalar + self.vector = np.array(vector) + + def __neg__(self): + return self.__class__(-self.scalar, -self.vector) + + def __mul__(self, other): + """ + Product of two quaternions + i*i = j*j = k*k = i*j*k = -1 + Quaternion multiplication can be expressed concisely + using scalar and vector parts, + see + """ + return self.__class__( + self.scalar*other.scalar - np.dot(self.vector, other.vector), + self.scalar*other.vector + self.vector*other.scalar + + np.cross(self.vector, other.vector)) + + def conjugate(self): + """The conjugate quaternion -(1/2)*(q+i*q*i+j*q*j+k*q*k)""" + return self.__class__(self.scalar, -self.vector) + + @property + def norm(self): + """The 2-norm, q*q', a scalar""" + return self.scalar*self.scalar + np.dot(self.vector, self.vector) + + def normalize(self): + """Scaling such that norm equals 1""" + n = np.sqrt(self.norm) + return self.__class__(self.scalar/n, self.vector/n) + + def reciprocal(self): + """The reciprocal, 1/q = q'/(q*q') = q' / norm(q)""" + n = self.norm + return self.__class__(self.scalar/n, -self.vector/n) + + def __div__(self, other): + return self*other.reciprocal() + + __truediv__ = __div__ + + def rotate(self, v): + # Rotate the vector v by the quaternion q, i.e., + # calculate (the vector part of) q*v/q + v = self.__class__(0, v) + v = self*v/self + return v.vector + + def __eq__(self, other): + return (self.scalar == other.scalar) and (self.vector == other.vector).all + + def __repr__(self): + return "_Quaternion({}, {})".format(repr(self.scalar), repr(self.vector)) + + @classmethod + def rotate_from_to(cls, r1, r2): + """ + The quaternion for the shortest rotation from vector r1 to vector r2 + i.e., q = sqrt(r2*r1'), normalized. + If r1 and r2 are antiparallel, then the result is ambiguous; + a normal vector will be returned, and a warning will be issued. + """ + k = np.cross(r1, r2) + nk = np.linalg.norm(k) + th = np.arctan2(nk, np.dot(r1, r2)) + th /= 2 + if nk == 0: # r1 and r2 are parallel or anti-parallel + if np.dot(r1, r2) < 0: + warnings.warn("Rotation defined by anti-parallel vectors is ambiguous") + k = np.zeros(3) + k[np.argmin(r1*r1)] = 1 # basis vector most perpendicular to r1-r2 + k = np.cross(r1, k) + k = k / np.linalg.norm(k) # unit vector normal to r1-r2 + q = cls(0, k) + else: + q = cls(1, [0, 0, 0]) # = 1, no rotation + else: + q = cls(np.cos(th), k*np.sin(th)/nk) + return q + + @classmethod + def from_cardan_angles(cls, elev, azim, roll): + """ + Converts the angles to a quaternion + q = exp((roll/2)*e_x)*exp((elev/2)*e_y)*exp((-azim/2)*e_z) + i.e., the angles are a kind of Tait-Bryan angles, -z,y',x". + The angles should be given in radians, not degrees. + """ + ca, sa = np.cos(azim/2), np.sin(azim/2) + ce, se = np.cos(elev/2), np.sin(elev/2) + cr, sr = np.cos(roll/2), np.sin(roll/2) + + qw = ca*ce*cr + sa*se*sr + qx = ca*ce*sr - sa*se*cr + qy = ca*se*cr + sa*ce*sr + qz = ca*se*sr - sa*ce*cr + return cls(qw, [qx, qy, qz]) + + def as_cardan_angles(self): + """ + The inverse of `from_cardan_angles()`. + Note that the angles returned are in radians, not degrees. + The angles are not sensitive to the quaternion's norm(). + """ + qw = self.scalar + qx, qy, qz = self.vector[..., :] + azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz) + elev = np.arcsin(np.clip(2*(qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz), -1, 1)) + roll = np.arctan2(2*(qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz) + return elev, azim, roll diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 79b78657bdb9..4da5031b990c 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -195,11 +195,6 @@ def set_ticks_position(self, position): position : {'lower', 'upper', 'both', 'default', 'none'} The position of the bolded axis lines, ticks, and tick labels. """ - if position in ['top', 'bottom']: - _api.warn_deprecated('3.8', name=f'{position=}', - obj_type='argument value', - alternative="'upper' or 'lower'") - return _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'], position=position) self._tick_position = position @@ -224,11 +219,6 @@ def set_label_position(self, position): position : {'lower', 'upper', 'both', 'default', 'none'} The position of the axis label. """ - if position in ['top', 'bottom']: - _api.warn_deprecated('3.8', name=f'{position=}', - obj_type='argument value', - alternative="'upper' or 'lower'") - return _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'], position=position) self._label_position = position @@ -586,7 +576,7 @@ def draw(self, renderer): # Calculate offset distances # A rough estimate; points are ambiguous since 3D plots rotate - reltoinches = self.figure.dpi_scale_trans.inverted() + reltoinches = self.get_figure(root=False).dpi_scale_trans.inverted() ax_inches = reltoinches.transform(self.axes.bbox.size) ax_points_estimate = sum(72. * ax_inches) deltas_per_point = 48 / ax_points_estimate diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 098a7b6f6667..923bd32c9ce0 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -23,18 +23,10 @@ def world_transformation(xmin, xmax, dy /= ay dz /= az - return np.array([[1/dx, 0, 0, -xmin/dx], - [0, 1/dy, 0, -ymin/dy], - [0, 0, 1/dz, -zmin/dz], - [0, 0, 0, 1]]) - - -@_api.deprecated("3.8") -def rotation_about_vector(v, angle): - """ - Produce a rotation matrix for an angle in radians about a vector. - """ - return _rotation_about_vector(v, angle) + return np.array([[1/dx, 0, 0, -xmin/dx], + [ 0, 1/dy, 0, -ymin/dy], + [ 0, 0, 1/dz, -zmin/dz], + [ 0, 0, 0, 1]]) def _rotation_about_vector(v, angle): @@ -116,32 +108,6 @@ def _view_transformation_uvw(u, v, w, E): return M -@_api.deprecated("3.8") -def view_transformation(E, R, V, roll): - """ - Return the view transformation matrix. - - Parameters - ---------- - E : 3-element numpy array - The coordinates of the eye/camera. - R : 3-element numpy array - The coordinates of the center of the view box. - V : 3-element numpy array - Unit vector in the direction of the vertical axis. - roll : float - The roll angle in radians. - """ - u, v, w = _view_axes(E, R, V, roll) - M = _view_transformation_uvw(u, v, w, E) - return M - - -@_api.deprecated("3.8") -def persp_transformation(zfront, zback, focal_length): - return _persp_transformation(zfront, zback, focal_length) - - def _persp_transformation(zfront, zback, focal_length): e = focal_length a = 1 # aspect ratio @@ -154,11 +120,6 @@ def _persp_transformation(zfront, zback, focal_length): return proj_matrix -@_api.deprecated("3.8") -def ortho_transformation(zfront, zback): - return _ortho_transformation(zfront, zback) - - def _ortho_transformation(zfront, zback): # note: w component in the resulting vector will be (zback-zfront), not 1 a = -(zfront + zback) @@ -171,21 +132,36 @@ def _ortho_transformation(zfront, zback): def _proj_transform_vec(vec, M): - vecw = np.dot(M, vec) + vecw = np.dot(M, vec.data) w = vecw[3] - # clip here.. txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w + if np.ma.isMA(vec[0]): # we check each to protect for scalars + txs = np.ma.array(txs, mask=vec[0].mask) + if np.ma.isMA(vec[1]): + tys = np.ma.array(tys, mask=vec[1].mask) + if np.ma.isMA(vec[2]): + tzs = np.ma.array(tzs, mask=vec[2].mask) return txs, tys, tzs -def _proj_transform_vec_clip(vec, M): - vecw = np.dot(M, vec) +def _proj_transform_vec_clip(vec, M, focal_length): + vecw = np.dot(M, vec.data) w = vecw[3] - # clip here. txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w - tis = (0 <= vecw[0]) & (vecw[0] <= 1) & (0 <= vecw[1]) & (vecw[1] <= 1) - if np.any(tis): - tis = vecw[1] < 1 + if np.isinf(focal_length): # don't clip orthographic projection + tis = np.ones(txs.shape, dtype=bool) + else: + tis = (-1 <= txs) & (txs <= 1) & (-1 <= tys) & (tys <= 1) & (tzs <= 0) + if np.ma.isMA(vec[0]): + tis = tis & ~vec[0].mask + if np.ma.isMA(vec[1]): + tis = tis & ~vec[1].mask + if np.ma.isMA(vec[2]): + tis = tis & ~vec[2].mask + + txs = np.ma.masked_array(txs, ~tis) + tys = np.ma.masked_array(tys, ~tis) + tzs = np.ma.masked_array(tzs, ~tis) return txs, tys, tzs, tis @@ -204,7 +180,10 @@ def inv_transform(xs, ys, zs, invM): def _vec_pad_ones(xs, ys, zs): - return np.array([xs, ys, zs, np.ones_like(xs)]) + if np.ma.isMA(xs) or np.ma.isMA(ys) or np.ma.isMA(zs): + return np.ma.array([xs, ys, zs, np.ones_like(xs)]) + else: + return np.array([xs, ys, zs, np.ones_like(xs)]) def proj_transform(xs, ys, zs, M): @@ -215,45 +194,26 @@ def proj_transform(xs, ys, zs, M): return _proj_transform_vec(vec, M) -transform = _api.deprecated( - "3.8", obj_type="function", name="transform", - alternative="proj_transform")(proj_transform) +@_api.deprecated("3.10") +def proj_transform_clip(xs, ys, zs, M): + return _proj_transform_clip(xs, ys, zs, M, focal_length=np.inf) -def proj_transform_clip(xs, ys, zs, M): +def _proj_transform_clip(xs, ys, zs, M, focal_length): """ Transform the points by the projection matrix and return the clipping result returns txs, tys, tzs, tis """ vec = _vec_pad_ones(xs, ys, zs) - return _proj_transform_vec_clip(vec, M) - - -@_api.deprecated("3.8") -def proj_points(points, M): - return _proj_points(points, M) + return _proj_transform_vec_clip(vec, M, focal_length) def _proj_points(points, M): return np.column_stack(_proj_trans_points(points, M)) -@_api.deprecated("3.8") -def proj_trans_points(points, M): - return _proj_trans_points(points, M) - - def _proj_trans_points(points, M): - xs, ys, zs = zip(*points) + points = np.asanyarray(points) + xs, ys, zs = points[:, 0], points[:, 1], points[:, 2] return proj_transform(xs, ys, zs, M) - - -@_api.deprecated("3.8") -def rot_x(V, alpha): - cosa, sina = np.cos(alpha), np.sin(alpha) - M1 = np.array([[1, 0, 0, 0], - [0, cosa, -sina, 0], - [0, sina, cosa, 0], - [0, 0, 0, 1]]) - return np.dot(M1, V) diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_polygon.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_polygon.png new file mode 100644 index 000000000000..f1f160fe5579 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_polygon.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_quad.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_quad.png new file mode 100644 index 000000000000..e405bcffb965 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/fill_between_quad.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py index 4d33636a0b05..174c12608ae9 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -3,7 +3,11 @@ import matplotlib.pyplot as plt from matplotlib.backend_bases import MouseEvent -from mpl_toolkits.mplot3d.art3d import Line3DCollection, Poly3DCollection +from mpl_toolkits.mplot3d.art3d import ( + Line3DCollection, + Poly3DCollection, + _all_points_on_plane, +) def test_scatter_3d_projection_conservation(): @@ -56,6 +60,37 @@ def test_zordered_error(): plt.draw() +def test_all_points_on_plane(): + # Non-coplanar points + points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert not _all_points_on_plane(*points.T) + + # Duplicate points + points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 0]]) + assert _all_points_on_plane(*points.T) + + # NaN values + points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, np.nan]]) + assert _all_points_on_plane(*points.T) + + # Less than 3 unique points + points = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + assert _all_points_on_plane(*points.T) + + # All points lie on a line + points = np.array([[0, 0, 0], [0, 1, 0], [0, 2, 0], [0, 3, 0]]) + assert _all_points_on_plane(*points.T) + + # All points lie on two lines, with antiparallel vectors + points = np.array([[-2, 2, 0], [-1, 1, 0], [1, -1, 0], + [0, 0, 0], [2, 0, 0], [1, 0, 0]]) + assert _all_points_on_plane(*points.T) + + # All points lie on a plane + points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0], [1, 1, 0], [1, 2, 0]]) + assert _all_points_on_plane(*points.T) + + def test_generate_normals(): # Smoke test for https://github.com/matplotlib/matplotlib/issues/29156 vertices = ((0, 0, 0), (0, 5, 0), (5, 5, 0), (5, 0, 0)) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 20b8dcd432db..ad952e4395af 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -6,6 +6,7 @@ import pytest from mpl_toolkits.mplot3d import Axes3D, axes3d, proj3d, art3d +from mpl_toolkits.mplot3d.axes3d import _Quaternion as Quaternion import matplotlib as mpl from matplotlib.backend_bases import (MouseButton, MouseEvent, NavigationToolbar2) @@ -508,10 +509,10 @@ def test_scatter3d_sorting(fig_ref, fig_test, depthshade): linewidths[0::2, 0::2] = 5 linewidths[1::2, 1::2] = 5 - x, y, z, sizes, facecolors, edgecolors, linewidths = [ + x, y, z, sizes, facecolors, edgecolors, linewidths = ( a.flatten() for a in [x, y, z, sizes, facecolors, edgecolors, linewidths] - ] + ) ax_ref = fig_ref.add_subplot(projection='3d') sets = (np.unique(a) for a in [sizes, facecolors, edgecolors, linewidths]) @@ -593,6 +594,48 @@ def test_plot_3d_from_2d(): ax.plot(xs, ys, zs=0, zdir='y') +@mpl3d_image_comparison(['fill_between_quad.png'], style='mpl20') +def test_fill_between_quad(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + theta = np.linspace(0, 2*np.pi, 50) + + x1 = np.cos(theta) + y1 = np.sin(theta) + z1 = 0.1 * np.sin(6 * theta) + + x2 = 0.6 * np.cos(theta) + y2 = 0.6 * np.sin(theta) + z2 = 2 + + where = (theta < np.pi/2) | (theta > 3*np.pi/2) + + # Since none of x1 == x2, y1 == y2, or z1 == z2 is True, the fill_between + # mode will map to 'quad' + ax.fill_between(x1, y1, z1, x2, y2, z2, + where=where, mode='auto', alpha=0.5, edgecolor='k') + + +@mpl3d_image_comparison(['fill_between_polygon.png'], style='mpl20') +def test_fill_between_polygon(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + theta = np.linspace(0, 2*np.pi, 50) + + x1 = x2 = theta + y1 = y2 = 0 + z1 = np.cos(theta) + z2 = z1 + 1 + + where = (theta < np.pi/2) | (theta > 3*np.pi/2) + + # Since x1 == x2 and y1 == y2, the fill_between mode will be 'polygon' + ax.fill_between(x1, y1, z1, x2, y2, z2, + where=where, mode='auto', edgecolor='k') + + @mpl3d_image_comparison(['surface3d.png'], style='mpl20') def test_surface3d(): # Remove this line when this test image is regenerated. @@ -625,8 +668,6 @@ def test_surface3d_label_offset_tick_position(): ax.set_ylabel("Y label") ax.set_zlabel("Z label") - ax.figure.canvas.draw() - @mpl3d_image_comparison(['surface3d_shaded.png'], style='mpl20') def test_surface3d_shaded(): @@ -1283,6 +1324,21 @@ def test_unautoscale(axis, auto): np.testing.assert_array_equal(get_lim(), (-0.5, 0.5)) +@check_figures_equal(extensions=["png"]) +def test_culling(fig_test, fig_ref): + xmins = (-100, -50) + for fig, xmin in zip((fig_test, fig_ref), xmins): + ax = fig.add_subplot(projection='3d') + n = abs(xmin) + 1 + xs = np.linspace(0, xmin, n) + ys = np.ones(n) + zs = np.zeros(n) + ax.plot(xs, ys, zs, 'k') + + ax.set(xlim=(-5, 5), ylim=(-5, 5), zlim=(-5, 5)) + ax.view_init(5, 180, 0) + + def test_axes3d_focal_length_checks(): fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -1323,6 +1379,45 @@ def test_axes3d_isometric(): ax.grid(True) +@check_figures_equal(extensions=["png"]) +def test_axlim_clip(fig_test, fig_ref): + # With axlim clipping + ax = fig_test.add_subplot(projection="3d") + x = np.linspace(0, 1, 11) + y = np.linspace(0, 1, 11) + X, Y = np.meshgrid(x, y) + Z = X + Y + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=True) + # This ax.plot is to cover the extra surface edge which is not clipped out + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=True) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=True) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=True) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=True) + ax.text(1.1, 0.5, 4, 'test', axlim_clip=True) # won't be visible + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + # With manual clipping + ax = fig_ref.add_subplot(projection="3d") + idx = (X <= 0.5) + X = X[idx].reshape(11, 6) + Y = Y[idx].reshape(11, 6) + Z = Z[idx].reshape(11, 6) + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=False) + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=False) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=False) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=False) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=False) + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + @pytest.mark.parametrize('value', [np.inf, np.nan]) @pytest.mark.parametrize(('setter', 'side'), [ ('set_xlim3d', 'left'), @@ -1459,8 +1554,9 @@ def test_calling_conventions(self): ax.voxels(x, y) # x, y, z are positional only - this passes them on as attributes of # Poly3DCollection - with pytest.raises(AttributeError): + with pytest.raises(AttributeError, match="keyword argument 'x'") as exec_info: ax.voxels(filled=filled, x=x, y=y, z=z) + assert exec_info.value.name == 'x' def test_line3d_set_get_data_3d(): @@ -1793,29 +1889,168 @@ def test_shared_axes_retick(): assert ax2.get_zlim() == (-0.5, 2.5) -def test_rotate(): +def test_quaternion(): + # 1: + q1 = Quaternion(1, [0, 0, 0]) + assert q1.scalar == 1 + assert (q1.vector == [0, 0, 0]).all + # __neg__: + assert (-q1).scalar == -1 + assert ((-q1).vector == [0, 0, 0]).all + # i, j, k: + qi = Quaternion(0, [1, 0, 0]) + assert qi.scalar == 0 + assert (qi.vector == [1, 0, 0]).all + qj = Quaternion(0, [0, 1, 0]) + assert qj.scalar == 0 + assert (qj.vector == [0, 1, 0]).all + qk = Quaternion(0, [0, 0, 1]) + assert qk.scalar == 0 + assert (qk.vector == [0, 0, 1]).all + # i^2 = j^2 = k^2 = -1: + assert qi*qi == -q1 + assert qj*qj == -q1 + assert qk*qk == -q1 + # identity: + assert q1*qi == qi + assert q1*qj == qj + assert q1*qk == qk + # i*j=k, j*k=i, k*i=j: + assert qi*qj == qk + assert qj*qk == qi + assert qk*qi == qj + assert qj*qi == -qk + assert qk*qj == -qi + assert qi*qk == -qj + # __mul__: + assert (Quaternion(2, [3, 4, 5]) * Quaternion(6, [7, 8, 9]) + == Quaternion(-86, [28, 48, 44])) + # conjugate(): + for q in [q1, qi, qj, qk]: + assert q.conjugate().scalar == q.scalar + assert (q.conjugate().vector == -q.vector).all + assert q.conjugate().conjugate() == q + assert ((q*q.conjugate()).vector == 0).all + # norm: + q0 = Quaternion(0, [0, 0, 0]) + assert q0.norm == 0 + assert q1.norm == 1 + assert qi.norm == 1 + assert qj.norm == 1 + assert qk.norm == 1 + for q in [q0, q1, qi, qj, qk]: + assert q.norm == (q*q.conjugate()).scalar + # normalize(): + for q in [ + Quaternion(2, [0, 0, 0]), + Quaternion(0, [3, 0, 0]), + Quaternion(0, [0, 4, 0]), + Quaternion(0, [0, 0, 5]), + Quaternion(6, [7, 8, 9]) + ]: + assert q.normalize().norm == 1 + # reciprocal(): + for q in [q1, qi, qj, qk]: + assert q*q.reciprocal() == q1 + assert q.reciprocal()*q == q1 + # rotate(): + assert (qi.rotate([1, 2, 3]) == np.array([1, -2, -3])).all + # rotate_from_to(): + for r1, r2, q in [ + ([1, 0, 0], [0, 1, 0], Quaternion(np.sqrt(1/2), [0, 0, np.sqrt(1/2)])), + ([1, 0, 0], [0, 0, 1], Quaternion(np.sqrt(1/2), [0, -np.sqrt(1/2), 0])), + ([1, 0, 0], [1, 0, 0], Quaternion(1, [0, 0, 0])) + ]: + assert Quaternion.rotate_from_to(r1, r2) == q + # rotate_from_to(), special case: + for r1 in [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 1]]: + r1 = np.array(r1) + with pytest.warns(UserWarning): + q = Quaternion.rotate_from_to(r1, -r1) + assert np.isclose(q.norm, 1) + assert np.dot(q.vector, r1) == 0 + # from_cardan_angles(), as_cardan_angles(): + for elev, azim, roll in [(0, 0, 0), + (90, 0, 0), (0, 90, 0), (0, 0, 90), + (0, 30, 30), (30, 0, 30), (30, 30, 0), + (47, 11, -24)]: + for mag in [1, 2]: + q = Quaternion.from_cardan_angles( + np.deg2rad(elev), np.deg2rad(azim), np.deg2rad(roll)) + assert np.isclose(q.norm, 1) + q = Quaternion(mag * q.scalar, mag * q.vector) + np.testing.assert_allclose(np.rad2deg(Quaternion.as_cardan_angles(q)), + (elev, azim, roll), atol=1e-6) + + +@pytest.mark.parametrize('style', + ('azel', 'trackball', 'sphere', 'arcball')) +def test_rotate(style): """Test rotating using the left mouse button.""" - for roll in [0, 30]: - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1, projection='3d') - ax.view_init(0, 0, roll) - ax.figure.canvas.draw() - - # drag mouse horizontally to change azimuth - dx = 0.1 - dy = 0.2 - ax._button_press( - mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0)) - ax._on_move( - mock_event(ax, button=MouseButton.LEFT, - xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h)) - ax.figure.canvas.draw() - roll_radians = np.deg2rad(ax.roll) - cs = np.cos(roll_radians) - sn = np.sin(roll_radians) - assert ax.elev == (-dy*180*cs + dx*180*sn) - assert ax.azim == (-dy*180*sn - dx*180*cs) - assert ax.roll == roll + if style == 'azel': + s = 0.5 + else: + s = mpl.rcParams['axes3d.trackballsize'] / 2 + s *= 0.5 + mpl.rcParams['axes3d.trackballborder'] = 0 + with mpl.rc_context({'axes3d.mouserotationstyle': style}): + for roll, dx, dy in [ + [0, 1, 0], + [30, 1, 0], + [0, 0, 1], + [30, 0, 1], + [0, 0.5, np.sqrt(3)/2], + [30, 0.5, np.sqrt(3)/2], + [0, 2, 0]]: + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1, projection='3d') + ax.view_init(0, 0, roll) + 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)) + ax.figure.canvas.draw() + + c = np.sqrt(3)/2 + expectations = { + ('azel', 0, 1, 0): (0, -45, 0), + ('azel', 0, 0, 1): (-45, 0, 0), + ('azel', 0, 0.5, c): (-38.971143, -22.5, 0), + ('azel', 0, 2, 0): (0, -90, 0), + ('azel', 30, 1, 0): (22.5, -38.971143, 30), + ('azel', 30, 0, 1): (-38.971143, -22.5, 30), + ('azel', 30, 0.5, c): (-22.5, -38.971143, 30), + + ('trackball', 0, 1, 0): (0, -28.64789, 0), + ('trackball', 0, 0, 1): (-28.64789, 0, 0), + ('trackball', 0, 0.5, c): (-24.531578, -15.277726, 3.340403), + ('trackball', 0, 2, 0): (0, -180/np.pi, 0), + ('trackball', 30, 1, 0): (13.869588, -25.319385, 26.87008), + ('trackball', 30, 0, 1): (-24.531578, -15.277726, 33.340403), + ('trackball', 30, 0.5, c): (-13.869588, -25.319385, 33.129920), + + ('sphere', 0, 1, 0): (0, -30, 0), + ('sphere', 0, 0, 1): (-30, 0, 0), + ('sphere', 0, 0.5, c): (-25.658906, -16.102114, 3.690068), + ('sphere', 0, 2, 0): (0, -90, 0), + ('sphere', 30, 1, 0): (14.477512, -26.565051, 26.565051), + ('sphere', 30, 0, 1): (-25.658906, -16.102114, 33.690068), + ('sphere', 30, 0.5, c): (-14.477512, -26.565051, 33.434949), + + ('arcball', 0, 1, 0): (0, -60, 0), + ('arcball', 0, 0, 1): (-60, 0, 0), + ('arcball', 0, 0.5, c): (-48.590378, -40.893395, 19.106605), + ('arcball', 0, 2, 0): (0, 180, 0), + ('arcball', 30, 1, 0): (25.658906, -56.309932, 16.102114), + ('arcball', 30, 0, 1): (-48.590378, -40.893395, 49.106605), + ('arcball', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)} + new_elev, new_azim, new_roll = expectations[(style, roll, dx, dy)] + np.testing.assert_allclose((ax.elev, ax.azim, ax.roll), + (new_elev, new_azim, new_roll), atol=1e-6) def test_pan(): @@ -1827,9 +2062,10 @@ def convert_lim(dmin, dmax): range_ = dmax - dmin return center, range_ - ax = plt.figure().add_subplot(projection='3d') + fig = plt.figure() + ax = fig.add_subplot(projection='3d') ax.scatter(0, 0, 0) - ax.figure.canvas.draw() + fig.canvas.draw() x_center0, x_range0 = convert_lim(*ax.get_xlim3d()) y_center0, y_range0 = convert_lim(*ax.get_ylim3d()) @@ -2284,7 +2520,7 @@ def test_view_init_vertical_axis( rtol = 2e-06 ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() # Assert the projection matrix: proj_actual = ax.get_proj() @@ -2310,7 +2546,7 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None: """ ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() proj_before = ax.get_proj() event_click = mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=1) @@ -2339,7 +2575,7 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None: def test_set_box_aspect_vertical_axis(vertical_axis, aspect_expected): ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() ax.set_box_aspect(None) diff --git a/meson.build b/meson.build index c022becfd9d9..a50f0b8f743a 100644 --- a/meson.build +++ b/meson.build @@ -36,7 +36,7 @@ py_mod = import('python') py3 = py_mod.find_installation(pure: false) py3_dep = py3.dependency() -pybind11_dep = dependency('pybind11', version: '>=2.6') +pybind11_dep = dependency('pybind11', version: '>=2.13.2') subdir('extern') subdir('src') diff --git a/pyproject.toml b/pyproject.toml index aa6aa2350627..832d76308e0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers=[ "License :: OSI Approved :: Python Software Foundation License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -41,16 +40,14 @@ dependencies = [ "pillow >= 8", "pyparsing >= 2.3.1", "python-dateutil >= 2.7", - "importlib-resources >= 3.2.0; python_version < '3.10'", ] -requires-python = ">=3.9" +requires-python = ">=3.10" [project.optional-dependencies] # Should be a copy of the build dependencies below. dev = [ "meson-python>=0.13.1,<0.17.0", - "numpy>=1.25", - "pybind11>=2.6,!=2.13.3", + "pybind11>=2.13.2,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. @@ -74,20 +71,8 @@ build-backend = "mesonpy" # Also keep in sync with optional dependencies above. requires = [ "meson-python>=0.13.1,<0.17.0", - "pybind11>=2.6,!=2.13.3", + "pybind11>=2.13.2,!=2.13.3", "setuptools_scm>=7", - - # Comments on numpy build requirement range: - # - # 1. >=2.0.x is the numpy requirement for wheel builds for distribution - # on PyPI - building against 2.x yields wheels that are also - # ABI-compatible with numpy 1.x at runtime. - # 2. Note that building against numpy 1.x works fine too - users and - # redistributors can do this by installing the numpy version they like - # and disabling build isolation. - # 3. The <2.3 upper bound is for matching the numpy deprecation policy, - # it should not be loosened. - "numpy>=2.0.0rc1,<2.3", ] [tool.meson-python.args] @@ -115,6 +100,10 @@ exclude = [ ".tox", ".eggs", ] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] ignore = [ "D100", "D101", @@ -135,42 +124,46 @@ ignore = [ "E741", "F841", ] -line-length = 88 +preview = true +explicit-preview-rules = true select = [ "D", "E", "F", "W", + # The following error codes require the preview mode to be enabled. + "E201", + "E202", + "E203", + "E221", + "E251", + "E261", + "E272", + "E703", ] -# The following error codes are not supported by ruff v0.0.240 +# The following error codes are not supported by ruff v0.2.0 # They are planned and should be selected once implemented # even if they are deselected by default. # These are primarily whitespace/corrected by autoformatters (which we don't use). # See https://github.com/charliermarsh/ruff/issues/2402 for status on implementation external = [ "E122", - "E201", - "E202", - "E203", - "E221", - "E251", - "E261", - "E272", "E302", - "E703", ] -target-version = "py39" - -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "doc/conf.py" = ["E402"] "galleries/examples/animation/frame_grabbing_sgskip.py" = ["E402"] +"galleries/examples/images_contours_and_fields/tricontour_demo.py" = ["E201"] +"galleries/examples/images_contours_and_fields/tripcolor_demo.py" = ["E201"] +"galleries/examples/images_contours_and_fields/triplot_demo.py" = ["E201"] "galleries/examples/lines_bars_and_markers/marker_reference.py" = ["E402"] "galleries/examples/misc/print_stdout_sgskip.py" = ["E402"] +"galleries/examples/misc/table_demo.py" = ["E201"] "galleries/examples/style_sheets/bmh.py" = ["E501"] "galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py" = ["E402"] "galleries/examples/text_labels_and_annotations/custom_legends.py" = ["E402"] @@ -187,18 +180,18 @@ convention = "numpy" "galleries/examples/user_interfaces/pylab_with_gtk4_sgskip.py" = ["E402"] "galleries/examples/userdemo/pgf_preamble_sgskip.py" = ["E402"] -"lib/matplotlib/__init__.py" = ["E402", "F401"] -"lib/matplotlib/_animation_data.py" = ["E501"] -"lib/matplotlib/_api/__init__.py" = ["F401"] -"lib/matplotlib/axes/__init__.py" = ["F401", "F403"] +"lib/matplotlib/_cm.py" = ["E202", "E203"] +"lib/matplotlib/_mathtext.py" = ["E221"] +"lib/matplotlib/_mathtext_data.py" = ["E203"] "lib/matplotlib/backends/backend_template.py" = ["F401"] -"lib/matplotlib/font_manager.py" = ["E501"] -"lib/matplotlib/image.py" = ["F401", "F403"] "lib/matplotlib/pylab.py" = ["F401", "F403"] -"lib/matplotlib/pyplot.py" = ["F401", "F811"] +"lib/matplotlib/pyplot.py" = ["F811"] "lib/matplotlib/tests/test_mathtext.py" = ["E501"] -"lib/mpl_toolkits/axisartist/__init__.py" = ["F401"] -"lib/pylab.py" = ["F401", "F403"] +"lib/matplotlib/transforms.py" = ["E201"] +"lib/matplotlib/tri/_triinterpolate.py" = ["E201", "E221"] +"lib/mpl_toolkits/axes_grid1/axes_size.py" = ["E272"] +"lib/mpl_toolkits/axisartist/angle_helper.py" = ["E221"] +"lib/mpl_toolkits/mplot3d/proj3d.py" = ["E201"] "galleries/users_explain/artists/paths.py" = ["E402"] "galleries/users_explain/artists/patheffects_guide.py" = ["E402"] @@ -229,7 +222,7 @@ enable_incomplete_feature = [ ] exclude = [ #stubtest - ".*/matplotlib/(sphinxext|backends|testing/jpl_units)", + ".*/matplotlib/(sphinxext|backends|pylab|testing/jpl_units)", #mypy precommit "galleries/", "doc/", @@ -241,7 +234,9 @@ exclude = [ "lib/matplotlib/tests/", # tinypages is used for testing the sphinx ext, # stubtest will import and run, opening a figure if not excluded - ".*/tinypages" + ".*/tinypages", + # pylab's numpy wildcard imports cause re-def failures since numpy 2.2 + "lib/matplotlib/pylab.py", ] files = [ "lib/matplotlib", @@ -260,6 +255,7 @@ ignore_directives = [ # sphinxext.redirect_from "redirect-from", # sphinx-design + "card", "dropdown", "grid", "tab-set", @@ -280,6 +276,8 @@ ignore_directives = [ "ifconfig", # sphinx.ext.inheritance_diagram "inheritance-diagram", + # sphinx-tags + "tags", # include directive is causing attribute errors "include" ] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt new file mode 100644 index 000000000000..4d2a098c3c4f --- /dev/null +++ b/requirements/dev/build-requirements.txt @@ -0,0 +1,3 @@ +pybind11>=2.13.2,!=2.13.3 +meson-python +setuptools-scm diff --git a/requirements/dev/dev-requirements.txt b/requirements/dev/dev-requirements.txt index 117fd8acd3e6..e5cbc1091bb2 100644 --- a/requirements/dev/dev-requirements.txt +++ b/requirements/dev/dev-requirements.txt @@ -1,3 +1,4 @@ +-r build-requirements.txt -r ../doc/doc-requirements.txt -r ../testing/all.txt -r ../testing/extra.txt diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index ee74d02f7146..77cb606130b0 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -7,7 +7,7 @@ # Install the documentation requirements with: # pip install -r requirements/doc/doc-requirements.txt # -sphinx>=3.0.0,!=6.1.2 +sphinx>=5.1.0,!=6.1.2 colorspacious ipython ipywidgets @@ -17,9 +17,10 @@ packaging>=20 pydata-sphinx-theme~=0.15.0 mpl-sphinx-theme~=3.9.0 pyyaml +PyStemmer sphinxcontrib-svg2pdfconverter>=1.1.0 sphinxcontrib-video>=0.2.1 sphinx-copybutton sphinx-design -sphinx-gallery>=0.12.0 -sphinx-tags>=0.3.0 +sphinx-gallery[parallel]>=0.12.0 +sphinx-tags>=0.4.0 diff --git a/requirements/testing/extra.txt b/requirements/testing/extra.txt index b3e9009b561c..a5c1bef5f03a 100644 --- a/requirements/testing/extra.txt +++ b/requirements/testing/extra.txt @@ -1,4 +1,4 @@ -# Extra pip requirements for the Python 3.9+ builds +# Extra pip requirements for the Python 3.10+ builds --prefer-binary ipykernel diff --git a/requirements/testing/minver.txt b/requirements/testing/minver.txt index 1a95367eff14..3932e68eb015 100644 --- a/requirements/testing/minver.txt +++ b/requirements/testing/minver.txt @@ -4,12 +4,12 @@ contourpy==1.0.1 cycler==0.10 fonttools==4.22.0 importlib-resources==3.2.0 -kiwisolver==1.3.1 +kiwisolver==1.3.2 meson-python==0.13.1 meson==1.1.0 numpy==1.23.0 packaging==20.0 -pillow==8.0.0 +pillow==8.3.2 pyparsing==2.3.1 pytest==7.0.0 python-dateutil==2.7 diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index a5ca15cfbdad..aa20581ee69b 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -1,7 +1,7 @@ # Extra pip requirements for the GitHub Actions mypy build -mypy==1.1.1 -typing-extensions>=4.1,<5 +mypy>=1.9 +typing-extensions>=4.6 # Extra stubs distributed separately from the main pypi package pandas-stubs @@ -18,12 +18,9 @@ contourpy>=1.0.1 cycler>=0.10 fonttools>=4.22.0 kiwisolver>=1.3.1 -numpy>=1.19 packaging>=20.0 pillow>=8 pyparsing>=2.3.1 python-dateutil>=2.7 setuptools_scm>=7 setuptools>=64 - -importlib-resources>=3.2.0 ; python_version < "3.10" diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index ce88f504dc1e..eed27323ba9e 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -29,6 +29,16 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) lastclippath(NULL), _fill_color(agg::rgba(1, 1, 1, 0)) { + if (dpi <= 0.0) { + throw std::range_error("dpi must be positive"); + } + + if (width >= 1 << 23 || height >= 1 << 23) { + throw std::range_error( + "Image size of " + std::to_string(width) + "x" + std::to_string(height) + + " pixels is too large. It must be less than 2^23 in each direction."); + } + unsigned stride(width * 4); pixBuffer = new agg::int8u[NUMBYTES]; diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 470d459de341..8010508ae920 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -6,8 +6,11 @@ #ifndef MPL_BACKEND_AGG_H #define MPL_BACKEND_AGG_H +#include + #include #include +#include #include "agg_alpha_mask_u8.h" #include "agg_conv_curve.h" @@ -40,6 +43,8 @@ #include "array.h" #include "agg_workaround.h" +namespace py = pybind11; + /**********************************************************************/ // a helper class to pass agg::buffer objects around. @@ -112,10 +117,10 @@ class RendererAgg typedef agg::renderer_scanline_bin_solid renderer_bin; typedef agg::rasterizer_scanline_aa rasterizer; - typedef agg::scanline_p8 scanline_p8; - typedef agg::scanline_bin scanline_bin; + typedef agg::scanline32_p8 scanline_p8; + typedef agg::scanline32_bin scanline_bin; typedef agg::amask_no_clip_gray8 alpha_mask_type; - typedef agg::scanline_u8_am scanline_am; + typedef agg::scanline32_u8_am scanline_am; typedef agg::renderer_base renderer_base_alpha_mask_type; typedef agg::renderer_scanline_aa_solid renderer_alpha_mask_type; @@ -345,7 +350,8 @@ RendererAgg::_draw_path(path_t &path, bool has_clippath, const facepair_t &face, agg::trans_affine hatch_trans; hatch_trans *= agg::trans_affine_scaling(1.0, -1.0); hatch_trans *= agg::trans_affine_translation(0.0, 1.0); - hatch_trans *= agg::trans_affine_scaling(hatch_size, hatch_size); + hatch_trans *= agg::trans_affine_scaling(static_cast(hatch_size), + static_cast(hatch_size)); hatch_path_trans_t hatch_path_trans(hatch_path, hatch_trans); hatch_path_curve_t hatch_path_curve(hatch_path_trans); hatch_path_stroke_t hatch_path_stroke(hatch_path_curve); @@ -728,22 +734,25 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in rendererBase.reset_clipping(true); if (angle != 0.0) { agg::rendering_buffer srcbuf( - image.data(), (unsigned)image.shape(1), + image.mutable_data(0, 0), (unsigned)image.shape(1), (unsigned)image.shape(0), (unsigned)image.shape(1)); agg::pixfmt_gray8 pixf_img(srcbuf); set_clipbox(gc.cliprect, theRasterizer); + auto image_height = static_cast(image.shape(0)), + image_width = static_cast(image.shape(1)); + agg::trans_affine mtx; - mtx *= agg::trans_affine_translation(0, -image.shape(0)); + mtx *= agg::trans_affine_translation(0, -image_height); mtx *= agg::trans_affine_rotation(-angle * (agg::pi / 180.0)); mtx *= agg::trans_affine_translation(x, y); agg::path_storage rect; rect.move_to(0, 0); - rect.line_to(image.shape(1), 0); - rect.line_to(image.shape(1), image.shape(0)); - rect.line_to(0, image.shape(0)); + rect.line_to(image_width, 0); + rect.line_to(image_width, image_height); + rect.line_to(0, image_height); rect.line_to(0, 0); agg::conv_transform rect2(rect, mtx); @@ -828,20 +837,24 @@ inline void RendererAgg::draw_image(GCAgg &gc, bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); agg::rendering_buffer buffer; - buffer.attach( - image.data(), (unsigned)image.shape(1), (unsigned)image.shape(0), -(int)image.shape(1) * 4); + buffer.attach(image.mutable_data(0, 0, 0), + (unsigned)image.shape(1), (unsigned)image.shape(0), + -(int)image.shape(1) * 4); pixfmt pixf(buffer); if (has_clippath) { agg::trans_affine mtx; agg::path_storage rect; - mtx *= agg::trans_affine_translation((int)x, (int)(height - (y + image.shape(0)))); + auto image_height = static_cast(image.shape(0)), + image_width = static_cast(image.shape(1)); + + mtx *= agg::trans_affine_translation((int)x, (int)(height - (y + image_height))); rect.move_to(0, 0); - rect.line_to(image.shape(1), 0); - rect.line_to(image.shape(1), image.shape(0)); - rect.line_to(0, image.shape(0)); + rect.line_to(image_width, 0); + rect.line_to(image_width, image_height); + rect.line_to(0, image_height); rect.line_to(0, 0); agg::conv_transform rect2(rect, mtx); @@ -913,6 +926,10 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, typedef PathSnapper snapped_t; typedef agg::conv_curve snapped_curve_t; typedef agg::conv_curve curve_t; + typedef Sketch sketch_clipped_t; + typedef Sketch sketch_curve_t; + typedef Sketch sketch_snapped_t; + typedef Sketch sketch_snapped_curve_t; size_t Npaths = path_generator.num_paths(); size_t Noffsets = safe_first_shape(offsets); @@ -988,31 +1005,29 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } + gc.isaa = antialiaseds(i % Naa); + transformed_path_t tpath(path, trans); + nan_removed_t nan_removed(tpath, true, has_codes); + clipped_t clipped(nan_removed, do_clip, width, height); if (check_snap) { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); snapped_t snapped( clipped, gc.snap_mode, path.total_vertices(), points_to_pixels(gc.linewidth)); if (has_codes) { snapped_curve_t curve(snapped); - _draw_path(curve, has_clippath, face, gc); + sketch_snapped_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(snapped, has_clippath, face, gc); + sketch_snapped_t sketch(snapped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } else { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); if (has_codes) { curve_t curve(clipped); - _draw_path(curve, has_clippath, face, gc); + sketch_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(clipped, has_clippath, face, gc); + sketch_clipped_t sketch(clipped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } } @@ -1226,14 +1241,27 @@ inline void RendererAgg::draw_gouraud_triangles(GCAgg &gc, ColorArray &colors, agg::trans_affine &trans) { + if (points.shape(0)) { + check_trailing_shape(points, "points", 3, 2); + } + if (colors.shape(0)) { + check_trailing_shape(colors, "colors", 3, 4); + } + if (points.shape(0) != colors.shape(0)) { + throw py::value_error( + "points and colors arrays must be the same length, got " + + std::to_string(points.shape(0)) + " points and " + + std::to_string(colors.shape(0)) + "colors"); + } + theRasterizer.reset_clipping(); rendererBase.reset_clipping(true); set_clipbox(gc.cliprect, theRasterizer); bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); for (int i = 0; i < points.shape(0); ++i) { - typename PointArray::sub_t point = points.subarray(i); - typename ColorArray::sub_t color = colors.subarray(i); + auto point = std::bind(points, i, std::placeholders::_1, std::placeholders::_2); + auto color = std::bind(colors, i, std::placeholders::_1, std::placeholders::_2); _draw_gouraud_triangle(point, color, trans, has_clippath); } diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 4fbf846d8cb4..e3e6be9a4532 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -4,6 +4,9 @@ /* Contains some simple types from the Agg backend that are also used by other modules */ +#include + +#include #include #include "agg_color_rgba.h" @@ -13,6 +16,8 @@ #include "py_adaptors.h" +namespace py = pybind11; + struct ClipPath { mpl::PathIterator path; @@ -121,4 +126,132 @@ class GCAgg GCAgg &operator=(const GCAgg &); }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"butt", agg::butt_cap}, + {"round", agg::round_cap}, + {"projecting", agg::square_cap}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"miter", agg::miter_join_revert}, + {"round", agg::round_join}, + {"bevel", agg::bevel_join}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + auto [path, trans] = + src.cast, agg::trans_affine>>(); + if (path) { + value.path = *path; + } + value.trans = trans; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); + + bool load(handle src, bool) { + auto [dash_offset, dashes_seq_or_none] = + src.cast>>(); + + if (!dashes_seq_or_none) { + return true; + } + + auto dashes_seq = *dashes_seq_or_none; + + auto nentries = dashes_seq.size(); + // If the dashpattern has odd length, iterate through it twice (in + // accordance with the pdf/ps/svg specs). + auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; + + for (py::size_t i = 0; i < dash_pattern_length; i += 2) { + auto length = dashes_seq[i % nentries].cast(); + auto skip = dashes_seq[(i + 1) % nentries].cast(); + + value.add_dash_pair(length, skip); + } + + value.set_dash_offset(dash_offset); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.scale = 0.0; + value.length = 0.0; + value.randomness = 0.0; + return true; + } + + auto params = src.cast>(); + std::tie(value.scale, value.length, value.randomness) = params; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); + + bool load(handle src, bool) { + value.linewidth = src.attr("_linewidth").cast(); + value.alpha = src.attr("_alpha").cast(); + value.forced_alpha = src.attr("_forced_alpha").cast(); + value.color = src.attr("_rgb").cast(); + value.isaa = src.attr("_antialiased").cast(); + value.cap = src.attr("_capstyle").cast(); + value.join = src.attr("_joinstyle").cast(); + value.dashes = src.attr("get_dashes")().cast(); + value.cliprect = src.attr("_cliprect").cast(); + value.clippath = src.attr("get_clip_path")().cast(); + value.snap_mode = src.attr("get_snap")().cast(); + value.hatchpath = src.attr("get_hatch_path")().cast(); + value.hatch_color = src.attr("get_hatch_color")().cast(); + value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); + value.sketch = src.attr("get_sketch_params")().cast(); + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index eaf4bf6f5f9d..269e2aaa9ee5 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -1,566 +1,283 @@ +#include +#include +#include #include "mplutils.h" -#include "numpy_cpp.h" #include "py_converters.h" #include "_backend_agg.h" -typedef struct -{ - PyObject_HEAD - RendererAgg *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyRendererAgg; - -static PyTypeObject PyRendererAggType; - -typedef struct -{ - PyObject_HEAD - BufferRegion *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyBufferRegion; - -static PyTypeObject PyBufferRegionType; - +namespace py = pybind11; +using namespace pybind11::literals; /********************************************************************** * BufferRegion * */ -static PyObject *PyBufferRegion_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyBufferRegion *self; - self = (PyBufferRegion *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static void PyBufferRegion_dealloc(PyBufferRegion *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - /* TODO: This doesn't seem to be used internally. Remove? */ -static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args) -{ - int x; - if (!PyArg_ParseTuple(args, "i:set_x", &x)) { - return NULL; - } - self->x->get_rect().x1 = x; - - Py_RETURN_NONE; -} - -static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_x(BufferRegion *self, int x) { - int y; - if (!PyArg_ParseTuple(args, "i:set_y", &y)) { - return NULL; - } - self->x->get_rect().y1 = y; - - Py_RETURN_NONE; + self->get_rect().x1 = x; } -static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_y(BufferRegion *self, int y) { - agg::rect_i rect = self->x->get_rect(); - - return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); + self->get_rect().y1 = y; } -int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) +static py::object +PyBufferRegion_get_extents(BufferRegion *self) { - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->get_data(); - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} + agg::rect_i rect = self->get_rect(); -static PyTypeObject *PyBufferRegion_init_type() -{ - static PyMethodDef methods[] = { - { "set_x", (PyCFunction)PyBufferRegion_set_x, METH_VARARGS, NULL }, - { "set_y", (PyCFunction)PyBufferRegion_set_y, METH_VARARGS, NULL }, - { "get_extents", (PyCFunction)PyBufferRegion_get_extents, METH_NOARGS, NULL }, - { NULL } - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyBufferRegion_get_buffer; - - PyBufferRegionType.tp_name = "matplotlib.backends._backend_agg.BufferRegion"; - PyBufferRegionType.tp_basicsize = sizeof(PyBufferRegion); - PyBufferRegionType.tp_dealloc = (destructor)PyBufferRegion_dealloc; - PyBufferRegionType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyBufferRegionType.tp_methods = methods; - PyBufferRegionType.tp_new = PyBufferRegion_new; - PyBufferRegionType.tp_as_buffer = &buffer_procs; - - return &PyBufferRegionType; + return py::make_tuple(rect.x1, rect.y1, rect.x2, rect.y2); } /********************************************************************** * RendererAgg * */ -static PyObject *PyRendererAgg_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyRendererAgg *self; - self = (PyRendererAgg *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyRendererAgg_init(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static void +PyRendererAgg_draw_path(RendererAgg *self, + GCAgg &gc, + mpl::PathIterator path, + agg::trans_affine trans, + py::object rgbFace) { - unsigned int width; - unsigned int height; - double dpi; - int debug = 0; - - if (!PyArg_ParseTuple(args, "IId|i:RendererAgg", &width, &height, &dpi, &debug)) { - return -1; + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } - if (dpi <= 0.0) { - PyErr_SetString(PyExc_ValueError, "dpi must be positive"); - return -1; - } - - if (width >= 1 << 16 || height >= 1 << 16) { - PyErr_Format( - PyExc_ValueError, - "Image size of %dx%d pixels is too large. " - "It must be less than 2^16 in each direction.", - width, height); - return -1; - } - - CALL_CPP_INIT("RendererAgg", self->x = new RendererAgg(width, height, dpi)) - - return 0; + self->draw_path(gc, path, trans, face); } -static void PyRendererAgg_dealloc(PyRendererAgg *self) +static void +PyRendererAgg_draw_text_image(RendererAgg *self, + py::array_t image_obj, + std::variant vx, + std::variant vy, + double angle, + GCAgg &gc) { - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - -static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; - agg::rgba face; - - if (!PyArg_ParseTuple(args, - "O&O&O&|O:draw_path", - &convert_gcagg, - &gc, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; - } - - if (!convert_face(faceobj, gc, &face)) { - return NULL; + int x, y; + + if (auto value = std::get_if(&vx)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="x", "obj_type"_a="parameter as float", + "alternative"_a="int(x)"); + x = static_cast(*value); + } else if (auto value = std::get_if(&vx)) { + x = *value; + } else { + throw std::runtime_error("Should not happen"); } - CALL_CPP("draw_path", (self->x->draw_path(gc, path, trans, face))); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *args) -{ - numpy::array_view image; - double x; - double y; - double angle; - GCAgg gc; - - if (!PyArg_ParseTuple(args, - "O&dddO&:draw_text_image", - &image.converter_contiguous, - &image, - &x, - &y, - &angle, - &convert_gcagg, - &gc)) { - return NULL; + if (auto value = std::get_if(&vy)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="y", "obj_type"_a="parameter as float", + "alternative"_a="int(y)"); + y = static_cast(*value); + } else if (auto value = std::get_if(&vy)) { + y = *value; + } else { + throw std::runtime_error("Should not happen"); } - CALL_CPP("draw_text_image", (self->x->draw_text_image(gc, image, x, y, angle))); + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<2>(); - Py_RETURN_NONE; + self->draw_text_image(gc, image, x, y, angle); } -PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_markers(RendererAgg *self, + GCAgg &gc, + mpl::PathIterator marker_path, + agg::trans_affine marker_path_trans, + mpl::PathIterator path, + agg::trans_affine trans, + py::object rgbFace) { - GCAgg gc; - mpl::PathIterator marker_path; - agg::trans_affine marker_path_trans; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; - agg::rgba face; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&|O:draw_markers", - &convert_gcagg, - &gc, - &convert_path, - &marker_path, - &convert_trans_affine, - &marker_path_trans, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } - if (!convert_face(faceobj, gc, &face)) { - return NULL; - } - - CALL_CPP("draw_markers", - (self->x->draw_markers(gc, marker_path, marker_path_trans, path, trans, face))); - - Py_RETURN_NONE; + self->draw_markers(gc, marker_path, marker_path_trans, path, trans, face); } -static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_image(RendererAgg *self, + GCAgg &gc, + double x, + double y, + py::array_t image_obj) { - GCAgg gc; - double x; - double y; - numpy::array_view image; - - if (!PyArg_ParseTuple(args, - "O&ddO&:draw_image", - &convert_gcagg, - &gc, - &x, - &y, - &image.converter_contiguous, - &image)) { - return NULL; - } + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<3>(); x = mpl_round(x); y = mpl_round(y); gc.alpha = 1.0; - CALL_CPP("draw_image", (self->x->draw_image(gc, x, y, image))); - - Py_RETURN_NONE; -} - -static PyObject * -PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - agg::trans_affine master_transform; - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; - agg::trans_affine offset_trans; - numpy::array_view facecolors; - numpy::array_view edgecolors; - numpy::array_view linewidths; - DashesVector dashes; - numpy::array_view antialiaseds; - PyObject *ignored; - PyObject *offset_position; // offset position is no longer used - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &convert_pathgen, - &paths, - &convert_transforms, - &transforms, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_colors, - &edgecolors, - &linewidths.converter, - &linewidths, - &convert_dashes_vector, - &dashes, - &antialiaseds.converter, - &antialiaseds, - &ignored, - &offset_position)) { - return NULL; - } - - CALL_CPP("draw_path_collection", - (self->x->draw_path_collection(gc, - master_transform, - paths, - transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - dashes, - antialiaseds))); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - agg::trans_affine master_transform; - unsigned int mesh_width; - unsigned int mesh_height; - numpy::array_view coordinates; - numpy::array_view offsets; - agg::trans_affine offset_trans; - numpy::array_view facecolors; - bool antialiased; - numpy::array_view edgecolors; - - if (!PyArg_ParseTuple(args, - "O&O&IIO&O&O&O&O&O&:draw_quad_mesh", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &mesh_width, - &mesh_height, - &coordinates.converter, - &coordinates, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_bool, - &antialiased, - &convert_colors, - &edgecolors)) { - return NULL; - } - - CALL_CPP("draw_quad_mesh", - (self->x->draw_quad_mesh(gc, - master_transform, - mesh_width, - mesh_height, - coordinates, - offsets, - offset_trans, - facecolors, - antialiased, - edgecolors))); - - Py_RETURN_NONE; -} - -static PyObject * -PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - numpy::array_view points; - numpy::array_view colors; - agg::trans_affine trans; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&|O:draw_gouraud_triangles", - &convert_gcagg, - &gc, - &points.converter, - &points, - &colors.converter, - &colors, - &convert_trans_affine, - &trans)) { - return NULL; - } - if (points.shape(0) && !check_trailing_shape(points, "points", 3, 2)) { - return NULL; - } - if (colors.shape(0) && !check_trailing_shape(colors, "colors", 3, 4)) { - return NULL; - } - if (points.shape(0) != colors.shape(0)) { - PyErr_Format(PyExc_ValueError, - "points and colors arrays must be the same length, got " - "%" NPY_INTP_FMT " points and %" NPY_INTP_FMT "colors", - points.shape(0), colors.shape(0)); - return NULL; - } - - CALL_CPP("draw_gouraud_triangles", self->x->draw_gouraud_triangles(gc, points, colors, trans)); - - Py_RETURN_NONE; + self->draw_image(gc, x, y, image); } -int PyRendererAgg_get_buffer(PyRendererAgg *self, Py_buffer *buf, int flags) +static void +PyRendererAgg_draw_path_collection(RendererAgg *self, + GCAgg &gc, + agg::trans_affine master_transform, + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans, + py::array_t facecolors_obj, + py::array_t edgecolors_obj, + py::array_t linewidths_obj, + DashesVector dashes, + py::array_t antialiaseds_obj, + py::object Py_UNUSED(ignored_obj), + // offset position is no longer used + py::object Py_UNUSED(offset_position_obj)) { - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->pixBuffer; - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + auto linewidths = linewidths_obj.unchecked<1>(); + auto antialiaseds = antialiaseds_obj.unchecked<1>(); + + self->draw_path_collection(gc, + master_transform, + paths, + transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + dashes, + antialiaseds); } -static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_quad_mesh(RendererAgg *self, + GCAgg &gc, + agg::trans_affine master_transform, + unsigned int mesh_width, + unsigned int mesh_height, + py::array_t coordinates_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans, + py::array_t facecolors_obj, + bool antialiased, + py::array_t edgecolors_obj) { - CALL_CPP("clear", self->x->clear()); - - Py_RETURN_NONE; + auto coordinates = coordinates_obj.mutable_unchecked<3>(); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + + self->draw_quad_mesh(gc, + master_transform, + mesh_width, + mesh_height, + coordinates, + offsets, + offset_trans, + facecolors, + antialiased, + edgecolors); } -static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, + GCAgg &gc, + py::array_t points_obj, + py::array_t colors_obj, + agg::trans_affine trans) { - agg::rect_d bbox; - BufferRegion *reg; - PyObject *regobj; - - if (!PyArg_ParseTuple(args, "O&:copy_from_bbox", &convert_rect, &bbox)) { - return 0; - } + auto points = points_obj.unchecked<3>(); + auto colors = colors_obj.unchecked<3>(); - CALL_CPP("copy_from_bbox", (reg = self->x->copy_from_bbox(bbox))); - - regobj = PyBufferRegion_new(&PyBufferRegionType, NULL, NULL); - ((PyBufferRegion *)regobj)->x = reg; - - return regobj; + self->draw_gouraud_triangles(gc, points, colors, trans); } -static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *args) +PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) { - PyBufferRegion *regobj; - int xx1 = 0, yy1 = 0, xx2 = 0, yy2 = 0, x = 0, y = 0; - - if (!PyArg_ParseTuple(args, - "O!|iiiiii:restore_region", - &PyBufferRegionType, - ®obj, - &xx1, - &yy1, - &xx2, - &yy2, - &x, - &y)) { - return 0; - } - - if (PySequence_Size(args) == 1) { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x))); - } else { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x), xx1, yy1, xx2, yy2, x, y)); - } - - Py_RETURN_NONE; -} - -static PyTypeObject *PyRendererAgg_init_type() -{ - static PyMethodDef methods[] = { - {"draw_path", (PyCFunction)PyRendererAgg_draw_path, METH_VARARGS, NULL}, - {"draw_markers", (PyCFunction)PyRendererAgg_draw_markers, METH_VARARGS, NULL}, - {"draw_text_image", (PyCFunction)PyRendererAgg_draw_text_image, METH_VARARGS, NULL}, - {"draw_image", (PyCFunction)PyRendererAgg_draw_image, METH_VARARGS, NULL}, - {"draw_path_collection", (PyCFunction)PyRendererAgg_draw_path_collection, METH_VARARGS, NULL}, - {"draw_quad_mesh", (PyCFunction)PyRendererAgg_draw_quad_mesh, METH_VARARGS, NULL}, - {"draw_gouraud_triangles", (PyCFunction)PyRendererAgg_draw_gouraud_triangles, METH_VARARGS, NULL}, - - {"clear", (PyCFunction)PyRendererAgg_clear, METH_NOARGS, NULL}, - - {"copy_from_bbox", (PyCFunction)PyRendererAgg_copy_from_bbox, METH_VARARGS, NULL}, - {"restore_region", (PyCFunction)PyRendererAgg_restore_region, METH_VARARGS, NULL}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyRendererAgg_get_buffer; - - PyRendererAggType.tp_name = "matplotlib.backends._backend_agg.RendererAgg"; - PyRendererAggType.tp_basicsize = sizeof(PyRendererAgg); - PyRendererAggType.tp_dealloc = (destructor)PyRendererAgg_dealloc; - PyRendererAggType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyRendererAggType.tp_methods = methods; - PyRendererAggType.tp_init = (initproc)PyRendererAgg_init; - PyRendererAggType.tp_new = PyRendererAgg_new; - PyRendererAggType.tp_as_buffer = &buffer_procs; - - return &PyRendererAggType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "_backend_agg" }; - -PyMODINIT_FUNC PyInit__backend_agg(void) -{ - import_array(); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) - || prepare_and_add_type(PyRendererAgg_init_type(), m) - // BufferRegion is not constructible from Python, thus not added to the module. - || PyType_Ready(PyBufferRegion_init_type()) - ) { - Py_XDECREF(m); - return NULL; - } - return m; + py::class_(m, "RendererAgg", py::buffer_protocol()) + .def(py::init(), + "width"_a, "height"_a, "dpi"_a) + + .def("draw_path", &PyRendererAgg_draw_path, + "gc"_a, "path"_a, "trans"_a, "face"_a = nullptr) + .def("draw_markers", &PyRendererAgg_draw_markers, + "gc"_a, "marker_path"_a, "marker_path_trans"_a, "path"_a, "trans"_a, + "face"_a = nullptr) + .def("draw_text_image", &PyRendererAgg_draw_text_image, + "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) + .def("draw_image", &PyRendererAgg_draw_image, + "gc"_a, "x"_a, "y"_a, "image"_a) + .def("draw_path_collection", &PyRendererAgg_draw_path_collection, + "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, + "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, + "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a) + .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, + "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, + "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, + "antialiased"_a, "edgecolors"_a) + .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, + "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) + + .def("clear", &RendererAgg::clear) + + .def("copy_from_bbox", &RendererAgg::copy_from_bbox, + "bbox"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a, "xx1"_a, "yy1"_a, "xx2"_a, "yy2"_a, "x"_a, "y"_a) + + .def_buffer([](RendererAgg *renderer) -> py::buffer_info { + std::vector shape { + renderer->get_height(), + renderer->get_width(), + 4 + }; + std::vector strides { + renderer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(renderer->pixBuffer, shape, strides); + }); + + py::class_(m, "BufferRegion", py::buffer_protocol()) + // BufferRegion is not constructible from Python, thus no py::init is added. + .def("set_x", &PyBufferRegion_set_x) + .def("set_y", &PyBufferRegion_set_y) + .def("get_extents", &PyBufferRegion_get_extents) + .def_buffer([](BufferRegion *buffer) -> py::buffer_info { + std::vector shape { + buffer->get_height(), + buffer->get_width(), + 4 + }; + std::vector strides { + buffer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(buffer->get_data(), shape, strides); + }); } diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 561cb303639c..db6191849bbe 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -196,7 +196,7 @@ mpl_SetProcessDpiAwareness_max(void) #endif } -PYBIND11_MODULE(_c_internal_utils, m) +PYBIND11_MODULE(_c_internal_utils, m, py::mod_gil_not_used()) { m.def( "display_is_valid", &mpl_display_is_valid, diff --git a/src/_enums.h b/src/_enums.h new file mode 100644 index 000000000000..18f3d9aac9fa --- /dev/null +++ b/src/_enums.h @@ -0,0 +1,95 @@ +#ifndef MPL_ENUMS_H +#define MPL_ENUMS_H + +#include + +// Extension for pybind11: Pythonic enums. +// This allows creating classes based on ``enum.*`` types. +// This code was copied from mplcairo, with some slight tweaks. +// The API is: +// +// - P11X_DECLARE_ENUM(py_name: str, py_base_cls: str, ...: {str, enum value}): +// py_name: The name to expose in the module. +// py_base_cls: The name of the enum base class to use. +// ...: The enum name/value pairs to expose. +// +// Use this macro to declare an enum and its values. +// +// - py11x::bind_enums(m: pybind11::module): +// m: The module to use to register the enum classes. +// +// Place this in PYBIND11_MODULE to register the enums declared by P11X_DECLARE_ENUM. + +// a1 includes the opening brace and a2 the closing brace. +// This definition is compatible with older compiler versions compared to +// #define P11X_ENUM_TYPE(...) decltype(std::map{std::pair __VA_ARGS__})::mapped_type +#define P11X_ENUM_TYPE(a1, a2, ...) decltype(std::pair a1, a2)::second_type + +#define P11X_CAT2(a, b) a##b +#define P11X_CAT(a, b) P11X_CAT2(a, b) + +namespace p11x { + namespace { + namespace py = pybind11; + + // Holder is (py_base_cls, [(name, value), ...]) before module init; + // converted to the Python class object after init. + auto enums = std::unordered_map{}; + + auto bind_enums(py::module mod) -> void + { + for (auto& [py_name, spec]: enums) { + auto const& [py_base_cls, pairs] = + spec.cast>(); + mod.attr(py::cast(py_name)) = spec = + py::module::import("enum").attr(py_base_cls.c_str())( + py_name, pairs, py::arg("module") = mod.attr("__name__")); + } + } + } +} + +// Immediately converting the args to a vector outside of the lambda avoids +// name collisions. +#define P11X_DECLARE_ENUM(py_name, py_base_cls, ...) \ + namespace p11x { \ + namespace { \ + [[maybe_unused]] auto const P11X_CAT(enum_placeholder_, __COUNTER__) = \ + [](auto args) { \ + py::gil_scoped_acquire gil; \ + using int_t = std::underlying_type_t; \ + auto pairs = std::vector>{}; \ + for (auto& [k, v]: args) { \ + pairs.emplace_back(k, int_t(v)); \ + } \ + p11x::enums[py_name] = pybind11::cast(std::pair{py_base_cls, pairs}); \ + return 0; \ + } (std::vector{std::pair __VA_ARGS__}); \ + } \ + } \ + namespace pybind11::detail { \ + template<> struct type_caster { \ + using type = P11X_ENUM_TYPE(__VA_ARGS__); \ + static_assert(std::is_enum_v, "Not an enum"); \ + PYBIND11_TYPE_CASTER(type, _(py_name)); \ + bool load(handle src, bool) { \ + auto cls = p11x::enums.at(py_name); \ + PyObject* tmp = nullptr; \ + if (pybind11::isinstance(src, cls) \ + && (tmp = PyNumber_Index(src.attr("value").ptr()))) { \ + auto ival = PyLong_AsLong(tmp); \ + value = decltype(value)(ival); \ + Py_DECREF(tmp); \ + return !(ival == -1 && !PyErr_Occurred()); \ + } else { \ + return false; \ + } \ + } \ + static handle cast(decltype(value) obj, return_value_policy, handle) { \ + auto cls = p11x::enums.at(py_name); \ + return cls(std::underlying_type_t(obj)).inc_ref(); \ + } \ + }; \ + } + +#endif /* MPL_ENUMS_H */ diff --git a/src/_image_resample.h b/src/_image_resample.h index a6404092ea2d..282bf8ef82f6 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -3,6 +3,8 @@ #ifndef MPL_RESAMPLE_H #define MPL_RESAMPLE_H +#define MPL_DISABLE_AGG_GRAY_CLIPPING + #include "agg_image_accessors.h" #include "agg_path_storage.h" #include "agg_pixfmt_gray.h" @@ -564,7 +566,8 @@ class span_conv_alpha { if (m_alpha != 1.0) { do { - span->a *= m_alpha; + span->a = static_cast( + static_cast(span->a) * m_alpha); ++span; } while (--len); } @@ -710,6 +713,7 @@ void resample( using renderer_t = agg::renderer_base; using rasterizer_t = agg::rasterizer_scanline_aa; + using scanline_t = agg::scanline32_u8; using reflect_t = agg::wrap_mode_reflect; using image_accessor_t = agg::image_accessor_wrap; @@ -737,7 +741,7 @@ void resample( span_alloc_t span_alloc; rasterizer_t rasterizer; - agg::scanline_u8 scanline; + scanline_t scanline; span_conv_alpha_t conv_alpha(params.alpha); diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 856dcf4ea3ce..0f7b0da88de8 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -2,7 +2,7 @@ #include #include "_image_resample.h" -#include "py_converters_11.h" +#include "py_converters.h" namespace py = pybind11; using namespace pybind11::literals; @@ -200,7 +200,8 @@ image_resample(py::array input_array, } -PYBIND11_MODULE(_image, m) { +PYBIND11_MODULE(_image, m, py::mod_gil_not_used()) +{ py::enum_(m, "_InterpolationType") .value("NEAREST", NEAREST) .value("BILINEAR", BILINEAR) diff --git a/src/_macosx.m b/src/_macosx.m index 1f291b52f6ba..30c5ddf30ce0 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -270,20 +270,46 @@ static CGFloat _get_device_scale(CGContextRef cr) return pixelSize.width; } -bool -mpl_check_modifier( - NSUInteger modifiers, NSEventModifierFlags flag, - PyObject* list, char const* name) -{ - bool failed = false; - if (modifiers & flag) { - PyObject* py_name = NULL; - if (!(py_name = PyUnicode_FromString(name)) - || PyList_Append(list, py_name)) { - failed = true; - } - Py_XDECREF(py_name); +bool mpl_check_button(bool present, PyObject* set, char const* name) { + PyObject* module = NULL, * cls = NULL, * button = NULL; + bool failed = ( + present + && (!(module = PyImport_ImportModule("matplotlib.backend_bases")) + || !(cls = PyObject_GetAttrString(module, "MouseButton")) + || !(button = PyObject_GetAttrString(cls, name)) + || PySet_Add(set, button))); + Py_XDECREF(module); + Py_XDECREF(cls); + Py_XDECREF(button); + return failed; +} + +PyObject* mpl_buttons() +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* set = NULL; + NSUInteger buttons = [NSEvent pressedMouseButtons]; + + if (!(set = PySet_New(NULL)) + || mpl_check_button(buttons & (1 << 0), set, "LEFT") + || mpl_check_button(buttons & (1 << 1), set, "RIGHT") + || mpl_check_button(buttons & (1 << 2), set, "MIDDLE") + || mpl_check_button(buttons & (1 << 3), set, "BACK") + || mpl_check_button(buttons & (1 << 4), set, "FORWARD")) { + Py_CLEAR(set); // On failure, return NULL with an exception set. } + PyGILState_Release(gstate); + return set; +} + +bool mpl_check_modifier(bool present, PyObject* list, char const* name) +{ + PyObject* py_name = NULL; + bool failed = ( + present + && (!(py_name = PyUnicode_FromString(name)) + || (PyList_Append(list, py_name)))); + Py_XDECREF(py_name); return failed; } @@ -291,17 +317,14 @@ static CGFloat _get_device_scale(CGContextRef cr) { PyGILState_STATE gstate = PyGILState_Ensure(); PyObject* list = NULL; - if (!(list = PyList_New(0))) { - goto exit; - } NSUInteger modifiers = [event modifierFlags]; - if (mpl_check_modifier(modifiers, NSEventModifierFlagControl, list, "ctrl") - || mpl_check_modifier(modifiers, NSEventModifierFlagOption, list, "alt") - || mpl_check_modifier(modifiers, NSEventModifierFlagShift, list, "shift") - || mpl_check_modifier(modifiers, NSEventModifierFlagCommand, list, "cmd")) { + if (!(list = PyList_New(0)) + || mpl_check_modifier(modifiers & NSEventModifierFlagControl, list, "ctrl") + || mpl_check_modifier(modifiers & NSEventModifierFlagOption, list, "alt") + || mpl_check_modifier(modifiers & NSEventModifierFlagShift, list, "shift") + || mpl_check_modifier(modifiers & NSEventModifierFlagCommand, list, "cmd")) { Py_CLEAR(list); // On failure, return NULL with an exception set. } -exit: PyGILState_Release(gstate); return list; } @@ -1238,7 +1261,7 @@ -(void)drawRect:(NSRect)rect CGContextRef cr = [[NSGraphicsContext currentContext] CGContext]; if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", "")) - || !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) { + || !(renderer_buffer = PyObject_CallMethod(renderer, "buffer_rgba", ""))) { PyErr_Print(); goto exit; } @@ -1450,9 +1473,9 @@ - (void)mouseMoved:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N, s:N}", "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, - "modifiers", mpl_modifiers(event)); + "buttons", mpl_buttons(), "modifiers", mpl_modifiers(event)); } - (void)mouseDragged:(NSEvent *)event @@ -1463,9 +1486,9 @@ - (void)mouseDragged:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N, s:N}", "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, - "modifiers", mpl_modifiers(event)); + "buttons", mpl_buttons(), "modifiers", mpl_modifiers(event)); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown: event]; } diff --git a/src/_path.h b/src/_path.h index 7f17d0bc2933..f5c06e4a6a15 100644 --- a/src/_path.h +++ b/src/_path.h @@ -18,7 +18,6 @@ #include "path_converters.h" #include "_backend_agg_basic_types.h" -#include "numpy_cpp.h" const size_t NUM_VERTICES[] = { 1, 1, 1, 2, 3 }; @@ -245,8 +244,7 @@ inline void points_in_path(PointArray &points, typedef agg::conv_curve curve_t; typedef agg::conv_contour contour_t; - size_t i; - for (i = 0; i < safe_first_shape(points); ++i) { + for (auto i = 0; i < safe_first_shape(points); ++i) { result[i] = false; } @@ -270,10 +268,11 @@ template inline bool point_in_path( double x, double y, const double r, PathIterator &path, agg::trans_affine &trans) { - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -292,10 +291,11 @@ inline bool point_on_path( typedef agg::conv_curve curve_t; typedef agg::conv_stroke stroke_t; - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -382,20 +382,19 @@ void get_path_collection_extents(agg::trans_affine &master_transform, throw std::runtime_error("Offsets array must have shape (N, 2)"); } - size_t Npaths = paths.size(); - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Npaths = paths.size(); + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; reset_limits(extent); - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); if (Ntransforms) { - size_t ti = i % Ntransforms; + py::ssize_t ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -429,24 +428,23 @@ void point_in_path_collection(double x, bool filled, std::vector &result) { - size_t Npaths = paths.size(); + auto Npaths = paths.size(); if (Npaths == 0) { return; } - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path = paths(i % Npaths); if (Ntransforms) { - size_t ti = i % Ntransforms; + auto ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -1005,7 +1003,7 @@ void convert_path_to_polygons(PathIterator &path, template void -__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) +__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) { unsigned code; double x, y; @@ -1013,7 +1011,7 @@ __cleanup_path(VertexSource &source, std::vector &vertices, std::vector< code = source.vertex(&x, &y); vertices.push_back(x); vertices.push_back(y); - codes.push_back((npy_uint8)code); + codes.push_back(static_cast(code)); } while (code != agg::path_cmd_stop); } @@ -1224,17 +1222,15 @@ bool convert_to_string(PathIterator &path, } template -bool is_sorted_and_has_non_nan(PyArrayObject *array) +bool is_sorted_and_has_non_nan(py::array_t array) { - char* ptr = PyArray_BYTES(array); - npy_intp size = PyArray_DIM(array, 0), - stride = PyArray_STRIDE(array, 0); + auto size = array.shape(0); using limits = std::numeric_limits; T last = limits::has_infinity ? -limits::infinity() : limits::min(); bool found_non_nan = false; - for (npy_intp i = 0; i < size; ++i, ptr += stride) { - T current = *(T*)ptr; + for (auto i = 0; i < size; ++i) { + T current = *array.data(i); // The following tests !isnan(current), but also works for integral // types. (The isnan(IntegralType) overload is absent on MSVC.) if (current == current) { diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index b4eb5d19177f..e8322cb51b7b 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -7,14 +7,11 @@ #include #include -#include "numpy_cpp.h" - #include "_path.h" #include "_backend_agg_basic_types.h" #include "py_adaptors.h" #include "py_converters.h" -#include "py_converters_11.h" namespace py = pybind11; using namespace pybind11::literals; @@ -44,17 +41,9 @@ static py::array_t Py_points_in_path(py::array_t points_obj, double r, mpl::PathIterator path, agg::trans_affine trans) { - numpy::array_view points; - - if (!convert_points(points_obj.ptr(), &points)) { - throw py::error_already_set(); - } + auto points = convert_points(points_obj); - if (!check_trailing_shape(points, "points", 2)) { - throw py::error_already_set(); - } - - py::ssize_t dims[] = { static_cast(points.size()) }; + py::ssize_t dims[] = { points.shape(0) }; py::array_t results(dims); auto results_mutable = results.mutable_unchecked<1>(); @@ -123,24 +112,15 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - py::object paths_obj, py::object transforms_obj, - py::object offsets_obj, agg::trans_affine offset_trans) + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans) { - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); extent_limits e; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - get_path_collection_extents( master_transform, paths, transforms, offsets, offset_trans, e); @@ -161,25 +141,15 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, - agg::trans_affine master_transform, py::object paths_obj, - py::object transforms_obj, py::object offsets_obj, + agg::trans_affine master_transform, mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, bool filled) { - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); std::vector result; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - point_in_path_collection(x, y, radius, master_transform, paths, transforms, offsets, offset_trans, filled, result); @@ -211,9 +181,7 @@ Py_affine_transform(py::array_t(); - if(!check_trailing_shape(vertices, "vertices", 2)) { - throw py::error_already_set(); - } + check_trailing_shape(vertices, "vertices", 2); py::ssize_t dims[] = { vertices.shape(0), 2 }; py::array_t result(dims); @@ -237,13 +205,9 @@ Py_affine_transform(py::array_t bboxes_obj) { - numpy::array_view bboxes; - - if (!convert_bboxes(bboxes_obj.ptr(), &bboxes)) { - throw py::error_already_set(); - } + auto bboxes = convert_bboxes(bboxes_obj); return count_bboxes_overlapping_bbox(bbox, bboxes); } @@ -298,7 +262,7 @@ Py_cleanup_path(mpl::PathIterator path, agg::trans_affine trans, bool remove_nan bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); std::vector vertices; - std::vector codes; + std::vector codes; cleanup_path(path, trans, remove_nans, do_clip, clip_rect, snap_mode, stroke_width, *simplify, return_curves, sketch, vertices, codes); @@ -381,54 +345,31 @@ Py_is_sorted_and_has_non_nan(py::object obj) { bool result; - PyArrayObject *array = (PyArrayObject *)PyArray_CheckFromAny( - obj.ptr(), NULL, 1, 1, NPY_ARRAY_NOTSWAPPED, NULL); - - if (array == NULL) { - throw py::error_already_set(); + py::array array = py::array::ensure(obj); + if (array.ndim() != 1) { + throw std::invalid_argument("array must be 1D"); } + auto dtype = array.dtype(); /* Handle just the most common types here, otherwise coerce to double */ - switch (PyArray_TYPE(array)) { - case NPY_INT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONGLONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_FLOAT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_DOUBLE: - result = is_sorted_and_has_non_nan(array); - break; - default: - Py_DECREF(array); - array = (PyArrayObject *)PyArray_FromObject(obj.ptr(), NPY_DOUBLE, 1, 1); - if (array == NULL) { - throw py::error_already_set(); - } - result = is_sorted_and_has_non_nan(array); + if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else { + array = py::array_t::ensure(obj); + result = is_sorted_and_has_non_nan(array); } - Py_DECREF(array); - return result; } -PYBIND11_MODULE(_path, m) +PYBIND11_MODULE(_path, m, py::mod_gil_not_used()) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - m.def("point_in_path", &Py_point_in_path, "x"_a, "y"_a, "radius"_a, "path"_a, "trans"_a); m.def("points_in_path", &Py_points_in_path, diff --git a/src/_qhull_wrapper.cpp b/src/_qhull_wrapper.cpp index 9784a1698ba1..da623a8d1b71 100644 --- a/src/_qhull_wrapper.cpp +++ b/src/_qhull_wrapper.cpp @@ -276,7 +276,8 @@ delaunay(const CoordArray& x, const CoordArray& y, int verbose) return delaunay_impl(npoints, x.data(), y.data(), verbose == 0); } -PYBIND11_MODULE(_qhull, m) { +PYBIND11_MODULE(_qhull, m, py::mod_gil_not_used()) +{ m.doc() = "Computing Delaunay triangulations.\n"; m.def("delaunay", &delaunay, "x"_a, "y"_a, "verbose"_a, diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index bfc2253188fd..874f6afb1b52 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -7,7 +7,7 @@ // and methods of operation are now quite different. Because our review of // the codebase showed that all the code that came from PIL was removed or // rewritten, we have removed the PIL licensing information. If you want PIL, -// you can get it at https://python-pillow.org/ +// you can get it at https://python-pillow.github.io #include #include @@ -92,6 +92,7 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; // Global vars for Tcl functions. We load these symbols from the tkinter // extension module or loaded Tcl libraries at run-time. static Tcl_SetVar_t TCL_SETVAR; +static Tcl_SetVar2_t TCL_SETVAR2; static void mpl_tk_blit(py::object interp_obj, const char *photo_name, @@ -173,7 +174,15 @@ DpiSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, std::string dpi = std::to_string(LOWORD(wParam)); Tcl_Interp* interp = (Tcl_Interp*)dwRefData; - TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0); + if (TCL_SETVAR) { + TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0); + } else if (TCL_SETVAR2) { + TCL_SETVAR2(interp, var_name.c_str(), NULL, dpi.c_str(), 0); + } else { + // This should be prevented at import time, and therefore unreachable. + // But defensively throw just in case. + throw std::runtime_error("Unable to call Tcl_SetVar or Tcl_SetVar2"); + } } return 0; case WM_NCDESTROY: @@ -246,13 +255,16 @@ bool load_tcl_tk(T lib) if (auto ptr = dlsym(lib, "Tcl_SetVar")) { TCL_SETVAR = (Tcl_SetVar_t)ptr; } + if (auto ptr = dlsym(lib, "Tcl_SetVar2")) { + TCL_SETVAR2 = (Tcl_SetVar2_t)ptr; + } if (auto ptr = dlsym(lib, "Tk_FindPhoto")) { TK_FIND_PHOTO = (Tk_FindPhoto_t)ptr; } if (auto ptr = dlsym(lib, "Tk_PhotoPutBlock")) { TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)ptr; } - return TCL_SETVAR && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK; + return (TCL_SETVAR || TCL_SETVAR2) && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK; } #ifdef WIN32_DLL @@ -333,7 +345,7 @@ load_tkinter_funcs() } #endif // end not Windows -PYBIND11_MODULE(_tkagg, m) +PYBIND11_MODULE(_tkagg, m, py::mod_gil_not_used()) { try { load_tkinter_funcs(); @@ -343,8 +355,8 @@ PYBIND11_MODULE(_tkagg, m) throw py::error_already_set(); } - if (!TCL_SETVAR) { - throw py::import_error("Failed to load Tcl_SetVar"); + if (!(TCL_SETVAR || TCL_SETVAR2)) { + throw py::import_error("Failed to load Tcl_SetVar or Tcl_SetVar2"); } else if (!TK_FIND_PHOTO) { throw py::import_error("Failed to load Tk_FindPhoto"); } else if (!TK_PHOTO_PUT_BLOCK) { diff --git a/src/_tkmini.h b/src/_tkmini.h index 85f245815e4c..1c74cf9720f8 100644 --- a/src/_tkmini.h +++ b/src/_tkmini.h @@ -104,6 +104,9 @@ typedef int (*Tk_PhotoPutBlock_t) (Tcl_Interp *interp, Tk_PhotoHandle handle, /* Tcl_SetVar typedef */ typedef const char *(*Tcl_SetVar_t)(Tcl_Interp *interp, const char *varName, const char *newValue, int flags); +/* Tcl_SetVar2 typedef */ +typedef const char *(*Tcl_SetVar2_t)(Tcl_Interp *interp, const char *part1, const char *part2, + const char *newValue, int flags); #ifdef __cplusplus } diff --git a/src/_ttconv.cpp b/src/_ttconv.cpp deleted file mode 100644 index a99ea9d1c891..000000000000 --- a/src/_ttconv.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* - _ttconv.c - - Python wrapper for TrueType conversion library in ../ttconv. - */ - -#include -#include "pprdrv.h" -#include - -namespace py = pybind11; -using namespace pybind11::literals; - -/** - * An implementation of TTStreamWriter that writes to a Python - * file-like object. - */ -class PythonFileWriter : public TTStreamWriter -{ - py::function _write_method; - - public: - PythonFileWriter(py::object& file_object) - : _write_method(file_object.attr("write")) {} - - virtual void write(const char *a) - { - PyObject* decoded = PyUnicode_DecodeLatin1(a, strlen(a), ""); - if (decoded == NULL) { - throw py::error_already_set(); - } - _write_method(py::handle(decoded)); - Py_DECREF(decoded); - } -}; - -static void convert_ttf_to_ps( - const char *filename, - py::object &output, - int fonttype, - py::iterable* glyph_ids) -{ - PythonFileWriter output_(output); - - std::vector glyph_ids_; - if (glyph_ids) { - for (py::handle glyph_id: *glyph_ids) { - glyph_ids_.push_back(glyph_id.cast()); - } - } - - if (fonttype != 3 && fonttype != 42) { - throw py::value_error( - "fonttype must be either 3 (raw Postscript) or 42 (embedded Truetype)"); - } - - try - { - insert_ttfont(filename, output_, static_cast(fonttype), glyph_ids_); - } - catch (TTException &e) - { - throw std::runtime_error(e.getMessage()); - } - catch (...) - { - throw std::runtime_error("Unknown C++ exception"); - } -} - -PYBIND11_MODULE(_ttconv, m) { - m.doc() = "Module to handle converting and subsetting TrueType " - "fonts to Postscript Type 3, Postscript Type 42 and " - "Pdf Type 3 fonts."; - m.def("convert_ttf_to_ps", &convert_ttf_to_ps, - "filename"_a, - "output"_a, - "fonttype"_a, - "glyph_ids"_a = py::none(), - "Converts the Truetype font into a Type 3 or Type 42 Postscript font, " - "optionally subsetting the font to only the desired set of characters.\n" - "\n" - "filename is the path to a TTF font file.\n" - "output is a Python file-like object with a write method that the Postscript " - "font data will be written to.\n" - "fonttype may be either 3 or 42. Type 3 is a \"raw Postscript\" font. " - "Type 42 is an embedded Truetype font. Glyph subsetting is not supported " - "for Type 42 fonts within this module (needs to be done externally).\n" - "glyph_ids (optional) is a list of glyph ids (integers) to keep when " - "subsetting to a Type 3 font. If glyph_ids is not provided or is None, " - "then all glyphs will be included. If any of the glyphs specified are " - "composite glyphs, then the component glyphs will also be included." - ); -} diff --git a/src/ft2font.cpp b/src/ft2font.cpp index b20f224715bf..c0e8b7c27125 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -1,18 +1,16 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ -#define NO_IMPORT_ARRAY - #include +#include #include #include #include #include #include +#include #include "ft2font.h" #include "mplutils.h" -#include "numpy_cpp.h" -#include "py_exceptions.h" #ifndef M_PI #define M_PI 3.14159265358979323846264338328 @@ -65,12 +63,12 @@ void throw_ft_error(std::string message, FT_Error error) { throw std::runtime_error(os.str()); } -FT2Image::FT2Image() : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0) +FT2Image::FT2Image() : m_buffer(NULL), m_width(0), m_height(0) { } FT2Image::FT2Image(unsigned long width, unsigned long height) - : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0) + : m_buffer(NULL), m_width(0), m_height(0) { resize(width, height); } @@ -104,8 +102,6 @@ void FT2Image::resize(long width, long height) if (numBytes && m_buffer) { memset(m_buffer, 0, numBytes); } - - m_dirty = true; } void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) @@ -143,29 +139,6 @@ void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) } else { throw std::runtime_error("Unknown pixel mode"); } - - m_dirty = true; -} - -void FT2Image::draw_rect(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1) -{ - if (x0 > m_width || x1 > m_width || y0 > m_height || y1 > m_height) { - throw std::runtime_error("Rect coords outside image bounds"); - } - - size_t top = y0 * m_width; - size_t bottom = y1 * m_width; - for (size_t i = x0; i < x1 + 1; ++i) { - m_buffer[i + top] = 255; - m_buffer[i + bottom] = 255; - } - - for (size_t j = y0 + 1; j < y1; ++j) { - m_buffer[x0 + j * m_width] = 255; - m_buffer[x1 + j * m_width] = 255; - } - - m_dirty = true; } void @@ -181,63 +154,28 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_buffer[i + j * m_width] = 255; } } - - m_dirty = true; } -static void ft_glyph_warn(FT_ULong charcode, std::set family_names) -{ - PyObject *text_helpers = NULL, *tmp = NULL; - std::set::iterator it = family_names.begin(); - std::stringstream ss; - ss<<*it; - while(++it != family_names.end()){ - ss<<", "<<*it; - } - - if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || - !(tmp = PyObject_CallMethod(text_helpers, - "warn_on_missing_glyph", "(k, s)", - charcode, ss.str().c_str()))) { - goto exit; - } -exit: - Py_XDECREF(text_helpers); - Py_XDECREF(tmp); - if (PyErr_Occurred()) { - throw mpl::exception(); - } -} - -// ft_outline_decomposer should be passed to FT_Outline_Decompose. On the -// first pass, vertices and codes are set to NULL, and index is simply -// incremented for each vertex that should be inserted, so that it is set, at -// the end, to the total number of vertices. On a second pass, vertices and -// codes should point to correctly sized arrays, and index set again to zero, -// to get fill vertices and codes with the outline decomposition. +// ft_outline_decomposer should be passed to FT_Outline_Decompose. struct ft_outline_decomposer { - int index; - double* vertices; - unsigned char* codes; + std::vector &vertices; + std::vector &codes; }; static int ft_outline_move_to(FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - if (d->index) { - // Appending CLOSEPOLY is important to make patheffects work. - *(d->vertices++) = 0; - *(d->vertices++) = 0; - *(d->codes++) = CLOSEPOLY; - } - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = MOVETO; - } - d->index += d->index ? 2 : 1; + if (!d->vertices.empty()) { + // Appending CLOSEPOLY is important to make patheffects work. + d->vertices.push_back(0); + d->vertices.push_back(0); + d->codes.push_back(CLOSEPOLY); + } + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(MOVETO); return 0; } @@ -245,12 +183,9 @@ static int ft_outline_line_to(FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = LINETO; - } - d->index++; + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(LINETO); return 0; } @@ -258,15 +193,12 @@ static int ft_outline_conic_to(FT_Vector const* control, FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = control->x * (1. / 64.); - *(d->vertices++) = control->y * (1. / 64.); - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = CURVE3; - *(d->codes++) = CURVE3; - } - d->index += 2; + d->vertices.push_back(control->x * (1. / 64.)); + d->vertices.push_back(control->y * (1. / 64.)); + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(CURVE3); + d->codes.push_back(CURVE3); return 0; } @@ -275,18 +207,15 @@ ft_outline_cubic_to( FT_Vector const* c1, FT_Vector const* c2, FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = c1->x * (1. / 64.); - *(d->vertices++) = c1->y * (1. / 64.); - *(d->vertices++) = c2->x * (1. / 64.); - *(d->vertices++) = c2->y * (1. / 64.); - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = CURVE4; - *(d->codes++) = CURVE4; - *(d->codes++) = CURVE4; - } - d->index += 3; + d->vertices.push_back(c1->x * (1. / 64.)); + d->vertices.push_back(c1->y * (1. / 64.)); + d->vertices.push_back(c2->x * (1. / 64.)); + d->vertices.push_back(c2->y * (1. / 64.)); + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(CURVE4); + d->codes.push_back(CURVE4); + d->codes.push_back(CURVE4); return 0; } @@ -296,52 +225,41 @@ static FT_Outline_Funcs ft_outline_funcs = { ft_outline_conic_to, ft_outline_cubic_to}; -PyObject* -FT2Font::get_path() +void +FT2Font::get_path(std::vector &vertices, std::vector &codes) { if (!face->glyph) { - PyErr_SetString(PyExc_RuntimeError, "No glyph loaded"); - return NULL; - } - ft_outline_decomposer decomposer = {}; - if (FT_Error error = - FT_Outline_Decompose( - &face->glyph->outline, &ft_outline_funcs, &decomposer)) { - PyErr_Format(PyExc_RuntimeError, - "FT_Outline_Decompose failed with error 0x%x", error); - return NULL; - } - if (!decomposer.index) { // Don't append CLOSEPOLY to null glyphs. - npy_intp vertices_dims[2] = { 0, 2 }; - numpy::array_view vertices(vertices_dims); - npy_intp codes_dims[1] = { 0 }; - numpy::array_view codes(codes_dims); - return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); - } - npy_intp vertices_dims[2] = { decomposer.index + 1, 2 }; - numpy::array_view vertices(vertices_dims); - npy_intp codes_dims[1] = { decomposer.index + 1 }; - numpy::array_view codes(codes_dims); - decomposer.index = 0; - decomposer.vertices = vertices.data(); - decomposer.codes = codes.data(); - if (FT_Error error = - FT_Outline_Decompose( - &face->glyph->outline, &ft_outline_funcs, &decomposer)) { - PyErr_Format(PyExc_RuntimeError, - "FT_Outline_Decompose failed with error 0x%x", error); - return NULL; - } - *(decomposer.vertices++) = 0; - *(decomposer.vertices++) = 0; - *(decomposer.codes++) = CLOSEPOLY; - return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); + throw std::runtime_error("No glyph loaded"); + } + ft_outline_decomposer decomposer = { + vertices, + codes, + }; + // We can make a close-enough estimate based on number of points and number of + // contours (which produce a MOVETO each), though it's slightly underestimating due + // to higher-order curves. + size_t estimated_points = static_cast(face->glyph->outline.n_contours) + + static_cast(face->glyph->outline.n_points); + vertices.reserve(2 * estimated_points); + codes.reserve(estimated_points); + if (FT_Error error = FT_Outline_Decompose( + &face->glyph->outline, &ft_outline_funcs, &decomposer)) { + throw std::runtime_error("FT_Outline_Decompose failed with error " + + std::to_string(error)); + } + if (vertices.empty()) { // Don't append CLOSEPOLY to null glyphs. + return; + } + vertices.push_back(0); + vertices.push_back(0); + codes.push_back(CLOSEPOLY); } FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_, - std::vector &fallback_list) - : image(), face(NULL) + std::vector &fallback_list, + FT2Font::WarnFunc warn) + : ft_glyph_warn(warn), image(), face(NULL) { clear(); @@ -386,8 +304,9 @@ FT2Font::~FT2Font() void FT2Font::clear() { - pen.x = 0; - pen.y = 0; + pen.x = pen.y = 0; + bbox.xMin = bbox.yMin = bbox.xMax = bbox.yMax = 0; + advance = 0; for (size_t i = 0; i < glyphs.size(); i++) { FT_Done_Glyph(glyphs[i]); @@ -435,7 +354,8 @@ void FT2Font::select_charmap(unsigned long i) } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback = false) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, + bool fallback = false) { if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && glyph_to_font.find(right) != glyph_to_font.end()) { @@ -456,7 +376,8 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallbac } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, + FT_Vector &delta) { if (!FT_HAS_KERNING(face)) { return 0; @@ -478,7 +399,7 @@ void FT2Font::set_kerning_factor(int factor) } void FT2Font::set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -501,7 +422,7 @@ void FT2Font::set_text( FT_UInt previous = 0; FT2Font *previous_ft_object = NULL; - for (size_t n = 0; n < N; n++) { + for (auto codepoint : text) { FT_UInt glyph_index = 0; FT_BBox glyph_bbox; FT_Pos last_advance; @@ -510,14 +431,14 @@ void FT2Font::set_text( std::set glyph_seen_fonts; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, glyph_to_font, codepoints[n], flags, + char_to_font, glyph_to_font, codepoint, flags, charcode_error, glyph_error, glyph_seen_fonts, false); if (!was_found) { - ft_glyph_warn((FT_ULong)codepoints[n], glyph_seen_fonts); + ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); // render missing glyph tofu // come back to top-most font ft_object_with_glyph = this; - char_to_font[codepoints[n]] = ft_object_with_glyph; + char_to_font[codepoint] = ft_object_with_glyph; glyph_to_font[glyph_index] = ft_object_with_glyph; ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); } @@ -771,29 +692,6 @@ void FT2Font::draw_glyphs_to_bitmap(bool antialiased) } } -void FT2Font::get_xys(bool antialiased, std::vector &xys) -{ - for (size_t n = 0; n < glyphs.size(); n++) { - - FT_Error error = FT_Glyph_To_Bitmap( - &glyphs[n], antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, 0, 1); - if (error) { - throw_ft_error("Could not convert glyph to bitmap", error); - } - - FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[n]; - - // bitmap left and top in pixel, string bbox in subpixel - FT_Int x = (FT_Int)(bitmap->left - bbox.xMin * (1. / 64.)); - FT_Int y = (FT_Int)(bbox.yMax * (1. / 64.) - bitmap->top + 1); - // make sure the index is non-neg - x = x < 0 ? 0 : x; - y = y < 0 ? 0 : y; - xys.push_back(x); - xys.push_back(y); - } -} - void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased) { FT_Vector sub_offset; @@ -819,7 +717,8 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback = false) +void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, + bool fallback = false) { if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { // cache is only for parent FT2Font @@ -830,11 +729,20 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallb if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ - PyOS_snprintf(buffer, 128, "uni%08x", glyph_number); + auto len = snprintf(buffer.data(), buffer.size(), "uni%08x", glyph_number); + if (len >= 0) { + buffer.resize(len); + } else { + throw std::runtime_error("Failed to convert glyph to standard name"); + } } else { - if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer, 128)) { + if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer.data(), buffer.size())) { throw_ft_error("Could not get glyph names", error); } + auto len = buffer.find('\0'); + if (len != buffer.npos) { + buffer.resize(len); + } } } diff --git a/src/ft2font.h b/src/ft2font.h index 66b218316e90..5524930d5ad0 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -6,11 +6,9 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H -#define PY_SSIZE_T_CLEAN -#include - -#include #include +#include +#include #include #include @@ -40,7 +38,6 @@ class FT2Image void resize(long width, long height); void draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y); - void draw_rect(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1); void draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1); unsigned char *get_buffer() @@ -57,7 +54,6 @@ class FT2Image } private: - bool m_dirty; unsigned char *m_buffer; unsigned long m_width; unsigned long m_height; @@ -71,18 +67,20 @@ extern FT_Library _ft2Library; class FT2Font { + typedef void (*WarnFunc)(FT_ULong charcode, std::set family_names); public: - FT2Font(FT_Open_Args &open_args, long hinting_factor, std::vector &fallback_list); + FT2Font(FT_Open_Args &open_args, long hinting_factor, + std::vector &fallback_list, WarnFunc warn); virtual ~FT2Font(); void clear(); void set_size(double ptsize, double dpi); void set_charmap(int i); void select_charmap(unsigned long i); - void set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys); - int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback); - int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta); + void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, + std::vector &xys); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, @@ -101,15 +99,12 @@ class FT2Font void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); long get_descent(); - // TODO: Since we know the size of the array upfront, we probably don't - // need to dynamically allocate like this - void get_xys(bool antialiased, std::vector &xys); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback); + void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); - PyObject* get_path(); + void get_path(std::vector &vertices, std::vector &codes); bool get_char_fallback_index(FT_ULong charcode, int& index) const; FT_Face const &get_face() const @@ -143,6 +138,7 @@ class FT2Font } private: + WarnFunc ft_glyph_warn; FT2Image image; FT_Face face; FT_Vector pen; /* untransformed origin */ diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 0fdb0165b462..7e1b3948a00e 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1,158 +1,266 @@ -#include "mplutils.h" -#include "ft2font.h" -#include "py_converters.h" -#include "py_exceptions.h" +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include +#include +#include -// From Python -#include +#include "ft2font.h" +#include "_enums.h" #include - -static PyObject *convert_xys_to_array(std::vector &xys) -{ - npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; - if (dims[0] > 0) { - return PyArray_SimpleNewFromData(2, dims, NPY_DOUBLE, &xys[0]); +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +template +using double_or_ = std::variant; + +template +static T +_double_to_(const char *name, double_or_ &var) +{ + if (auto value = std::get_if(&var)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a=name, "obj_type"_a="parameter as float", + "alternative"_a="int({})"_s.format(name)); + return static_cast(*value); + } else if (auto value = std::get_if(&var)) { + return *value; } else { - return PyArray_SimpleNew(2, dims, NPY_DOUBLE); + // pybind11 will have only allowed types that match the variant, so this `else` + // can't happen. We only have this case because older macOS doesn't support + // `std::get` and using the conditional `std::get_if` means an `else` to silence + // compiler warnings about "unhandled" cases. + throw std::runtime_error("Should not happen"); } } /********************************************************************** - * FT2Image + * Enumerations * */ -typedef struct -{ - PyObject_HEAD - FT2Image *x; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; -} PyFT2Image; - -static PyTypeObject PyFT2ImageType; - -static PyObject *PyFT2Image_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyFT2Image *self; - self = (PyFT2Image *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyFT2Image_init(PyFT2Image *self, PyObject *args, PyObject *kwds) -{ - double width; - double height; - - if (!PyArg_ParseTuple(args, "dd:FT2Image", &width, &height)) { - return -1; - } - - CALL_CPP_INIT("FT2Image", (self->x = new FT2Image(width, height))); - - return 0; -} - -static void PyFT2Image_dealloc(PyFT2Image *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - -const char *PyFT2Image_draw_rect__doc__ = - "draw_rect(self, x0, y0, x1, y1)\n" - "--\n\n" - "Draw an empty rectangle to the image.\n" - "\n" - ".. deprecated:: 3.8\n"; -; - -static PyObject *PyFT2Image_draw_rect(PyFT2Image *self, PyObject *args) -{ - char const* msg = - "FT2Image.draw_rect is deprecated since Matplotlib 3.8 and will be removed " - "in Matplotlib 3.10 as it is not used in the library. If you rely on it, " - "please let us know."; - if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { - return NULL; - } - - double x0, y0, x1, y1; - - if (!PyArg_ParseTuple(args, "dddd:draw_rect", &x0, &y0, &x1, &y1)) { - return NULL; - } +const char *Kerning__doc__ = R"""( + Kerning modes for `.FT2Font.get_kerning`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +P11X_DECLARE_ENUM( + "Kerning", "Enum", + {"DEFAULT", FT_KERNING_DEFAULT}, + {"UNFITTED", FT_KERNING_UNFITTED}, + {"UNSCALED", FT_KERNING_UNSCALED}, +); + +const char *FaceFlags__doc__ = R"""( + Flags returned by `FT2Font.face_flags`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class FaceFlags : FT_Long { +#define DECLARE_FLAG(name) name = FT_FACE_FLAG_##name + DECLARE_FLAG(SCALABLE), + DECLARE_FLAG(FIXED_SIZES), + DECLARE_FLAG(FIXED_WIDTH), + DECLARE_FLAG(SFNT), + DECLARE_FLAG(HORIZONTAL), + DECLARE_FLAG(VERTICAL), + DECLARE_FLAG(KERNING), + DECLARE_FLAG(FAST_GLYPHS), + DECLARE_FLAG(MULTIPLE_MASTERS), + DECLARE_FLAG(GLYPH_NAMES), + DECLARE_FLAG(EXTERNAL_STREAM), + DECLARE_FLAG(HINTER), + DECLARE_FLAG(CID_KEYED), + DECLARE_FLAG(TRICKY), + DECLARE_FLAG(COLOR), +#ifdef FT_FACE_FLAG_VARIATION // backcompat: ft 2.9.0. + DECLARE_FLAG(VARIATION), +#endif +#ifdef FT_FACE_FLAG_SVG // backcompat: ft 2.12.0. + DECLARE_FLAG(SVG), +#endif +#ifdef FT_FACE_FLAG_SBIX // backcompat: ft 2.12.0. + DECLARE_FLAG(SBIX), +#endif +#ifdef FT_FACE_FLAG_SBIX_OVERLAY // backcompat: ft 2.12.0. + DECLARE_FLAG(SBIX_OVERLAY), +#endif +#undef DECLARE_FLAG +}; - CALL_CPP("draw_rect", (self->x->draw_rect(x0, y0, x1, y1))); +P11X_DECLARE_ENUM( + "FaceFlags", "Flag", + {"SCALABLE", FaceFlags::SCALABLE}, + {"FIXED_SIZES", FaceFlags::FIXED_SIZES}, + {"FIXED_WIDTH", FaceFlags::FIXED_WIDTH}, + {"SFNT", FaceFlags::SFNT}, + {"HORIZONTAL", FaceFlags::HORIZONTAL}, + {"VERTICAL", FaceFlags::VERTICAL}, + {"KERNING", FaceFlags::KERNING}, + {"FAST_GLYPHS", FaceFlags::FAST_GLYPHS}, + {"MULTIPLE_MASTERS", FaceFlags::MULTIPLE_MASTERS}, + {"GLYPH_NAMES", FaceFlags::GLYPH_NAMES}, + {"EXTERNAL_STREAM", FaceFlags::EXTERNAL_STREAM}, + {"HINTER", FaceFlags::HINTER}, + {"CID_KEYED", FaceFlags::CID_KEYED}, + {"TRICKY", FaceFlags::TRICKY}, + {"COLOR", FaceFlags::COLOR}, + // backcompat: ft 2.9.0. + // {"VARIATION", FaceFlags::VARIATION}, + // backcompat: ft 2.12.0. + // {"SVG", FaceFlags::SVG}, + // backcompat: ft 2.12.0. + // {"SBIX", FaceFlags::SBIX}, + // backcompat: ft 2.12.0. + // {"SBIX_OVERLAY", FaceFlags::SBIX_OVERLAY}, +); + +const char *LoadFlags__doc__ = R"""( + Flags for `FT2Font.load_char`, `FT2Font.load_glyph`, and `FT2Font.set_text`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class LoadFlags : FT_Int32 { +#define DECLARE_FLAG(name) name = FT_LOAD_##name + DECLARE_FLAG(DEFAULT), + DECLARE_FLAG(NO_SCALE), + DECLARE_FLAG(NO_HINTING), + DECLARE_FLAG(RENDER), + DECLARE_FLAG(NO_BITMAP), + DECLARE_FLAG(VERTICAL_LAYOUT), + DECLARE_FLAG(FORCE_AUTOHINT), + DECLARE_FLAG(CROP_BITMAP), + DECLARE_FLAG(PEDANTIC), + DECLARE_FLAG(IGNORE_GLOBAL_ADVANCE_WIDTH), + DECLARE_FLAG(NO_RECURSE), + DECLARE_FLAG(IGNORE_TRANSFORM), + DECLARE_FLAG(MONOCHROME), + DECLARE_FLAG(LINEAR_DESIGN), + DECLARE_FLAG(NO_AUTOHINT), + DECLARE_FLAG(COLOR), +#ifdef FT_LOAD_COMPUTE_METRICS // backcompat: ft 2.6.1. + DECLARE_FLAG(COMPUTE_METRICS), +#endif +#ifdef FT_LOAD_BITMAP_METRICS_ONLY // backcompat: ft 2.7.1. + DECLARE_FLAG(BITMAP_METRICS_ONLY), +#endif +#ifdef FT_LOAD_NO_SVG // backcompat: ft 2.13.1. + DECLARE_FLAG(NO_SVG), +#endif + DECLARE_FLAG(TARGET_NORMAL), + DECLARE_FLAG(TARGET_LIGHT), + DECLARE_FLAG(TARGET_MONO), + DECLARE_FLAG(TARGET_LCD), + DECLARE_FLAG(TARGET_LCD_V), +#undef DECLARE_FLAG +}; - Py_RETURN_NONE; -} +P11X_DECLARE_ENUM( + "LoadFlags", "Flag", + {"DEFAULT", LoadFlags::DEFAULT}, + {"NO_SCALE", LoadFlags::NO_SCALE}, + {"NO_HINTING", LoadFlags::NO_HINTING}, + {"RENDER", LoadFlags::RENDER}, + {"NO_BITMAP", LoadFlags::NO_BITMAP}, + {"VERTICAL_LAYOUT", LoadFlags::VERTICAL_LAYOUT}, + {"FORCE_AUTOHINT", LoadFlags::FORCE_AUTOHINT}, + {"CROP_BITMAP", LoadFlags::CROP_BITMAP}, + {"PEDANTIC", LoadFlags::PEDANTIC}, + {"IGNORE_GLOBAL_ADVANCE_WIDTH", LoadFlags::IGNORE_GLOBAL_ADVANCE_WIDTH}, + {"NO_RECURSE", LoadFlags::NO_RECURSE}, + {"IGNORE_TRANSFORM", LoadFlags::IGNORE_TRANSFORM}, + {"MONOCHROME", LoadFlags::MONOCHROME}, + {"LINEAR_DESIGN", LoadFlags::LINEAR_DESIGN}, + {"NO_AUTOHINT", LoadFlags::NO_AUTOHINT}, + {"COLOR", LoadFlags::COLOR}, + // backcompat: ft 2.6.1. + {"COMPUTE_METRICS", LoadFlags::COMPUTE_METRICS}, + // backcompat: ft 2.7.1. + // {"BITMAP_METRICS_ONLY", LoadFlags::BITMAP_METRICS_ONLY}, + // backcompat: ft 2.13.1. + // {"NO_SVG", LoadFlags::NO_SVG}, + // These must be unique, but the others can be OR'd together; I don't know if + // there's any way to really enforce that. + {"TARGET_NORMAL", LoadFlags::TARGET_NORMAL}, + {"TARGET_LIGHT", LoadFlags::TARGET_LIGHT}, + {"TARGET_MONO", LoadFlags::TARGET_MONO}, + {"TARGET_LCD", LoadFlags::TARGET_LCD}, + {"TARGET_LCD_V", LoadFlags::TARGET_LCD_V}, +); + +const char *StyleFlags__doc__ = R"""( + Flags returned by `FT2Font.style_flags`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class StyleFlags : FT_Long { +#define DECLARE_FLAG(name) name = FT_STYLE_FLAG_##name + NORMAL = 0, + DECLARE_FLAG(ITALIC), + DECLARE_FLAG(BOLD), +#undef DECLARE_FLAG +}; -const char *PyFT2Image_draw_rect_filled__doc__ = - "draw_rect_filled(self, x0, y0, x1, y1)\n" - "--\n\n" - "Draw a filled rectangle to the image.\n"; +P11X_DECLARE_ENUM( + "StyleFlags", "Flag", + {"NORMAL", StyleFlags::NORMAL}, + {"ITALIC", StyleFlags::ITALIC}, + {"BOLD", StyleFlags::BOLD}, +); -static PyObject *PyFT2Image_draw_rect_filled(PyFT2Image *self, PyObject *args) -{ - double x0, y0, x1, y1; +/********************************************************************** + * FT2Image + * */ - if (!PyArg_ParseTuple(args, "dddd:draw_rect_filled", &x0, &y0, &x1, &y1)) { - return NULL; - } +const char *PyFT2Image__doc__ = R"""( + An image buffer for drawing glyphs. +)"""; - CALL_CPP("draw_rect_filled", (self->x->draw_rect_filled(x0, y0, x1, y1))); +const char *PyFT2Image_init__doc__ = R"""( + Parameters + ---------- + width, height : int + The dimensions of the image buffer. +)"""; - Py_RETURN_NONE; -} +const char *PyFT2Image_draw_rect_filled__doc__ = R"""( + Draw a filled rectangle to the image. -static int PyFT2Image_get_buffer(PyFT2Image *self, Py_buffer *buf, int flags) -{ - FT2Image *im = self->x; - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im->get_buffer(); - buf->len = im->get_width() * im->get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im->get_height(); - self->shape[1] = im->get_width(); - buf->shape = self->shape; - self->strides[0] = im->get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} + Parameters + ---------- + x0, y0, x1, y1 : float + The bounds of the rectangle from (x0, y0) to (x1, y1). +)"""; -static PyTypeObject* PyFT2Image_init_type() +static void +PyFT2Image_draw_rect_filled(FT2Image *self, + double_or_ vx0, double_or_ vy0, + double_or_ vx1, double_or_ vy1) { - static PyMethodDef methods[] = { - {"draw_rect", (PyCFunction)PyFT2Image_draw_rect, METH_VARARGS, PyFT2Image_draw_rect__doc__}, - {"draw_rect_filled", (PyCFunction)PyFT2Image_draw_rect_filled, METH_VARARGS, PyFT2Image_draw_rect_filled__doc__}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Image_get_buffer; + auto x0 = _double_to_("x0", vx0); + auto y0 = _double_to_("y0", vy0); + auto x1 = _double_to_("x1", vx1); + auto y1 = _double_to_("y1", vy1); - PyFT2ImageType.tp_name = "matplotlib.ft2font.FT2Image"; - PyFT2ImageType.tp_basicsize = sizeof(PyFT2Image); - PyFT2ImageType.tp_dealloc = (destructor)PyFT2Image_dealloc; - PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyFT2ImageType.tp_methods = methods; - PyFT2ImageType.tp_new = PyFT2Image_new; - PyFT2ImageType.tp_init = (initproc)PyFT2Image_init; - PyFT2ImageType.tp_as_buffer = &buffer_procs; - - return &PyFT2ImageType; + self->draw_rect_filled(x0, y0, x1, y1); } /********************************************************************** @@ -161,7 +269,6 @@ static PyTypeObject* PyFT2Image_init_type() typedef struct { - PyObject_HEAD size_t glyphInd; long width; long height; @@ -175,16 +282,25 @@ typedef struct FT_BBox bbox; } PyGlyph; -static PyTypeObject PyGlyphType; +const char *PyGlyph__doc__ = R"""( + Information about a single glyph. + + You cannot create instances of this object yourself, but must use + `.FT2Font.load_char` or `.FT2Font.load_glyph` to generate one. This object may be + used in a call to `.FT2Font.draw_glyph_to_bitmap`. -static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) + For more information on the various metrics, see `the FreeType documentation + `_. +)"""; + +static PyGlyph * +PyGlyph_from_FT2Font(const FT2Font *font) { const FT_Face &face = font->get_face(); const long hinting_factor = font->get_hinting_factor(); const FT_Glyph &glyph = font->get_last_glyph(); - PyGlyph *self; - self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); + PyGlyph *self = new PyGlyph(); self->glyphInd = font->get_last_glyph_index(); FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); @@ -199,48 +315,14 @@ static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) self->vertBearingY = face->glyph->metrics.vertBearingY; self->vertAdvance = face->glyph->metrics.vertAdvance; - return (PyObject *)self; -} - -static void PyGlyph_dealloc(PyGlyph *self) -{ - Py_TYPE(self)->tp_free((PyObject *)self); -} - -static PyObject *PyGlyph_get_bbox(PyGlyph *self, void *closure) -{ - return Py_BuildValue( - "llll", self->bbox.xMin, self->bbox.yMin, self->bbox.xMax, self->bbox.yMax); + return self; } -static PyTypeObject *PyGlyph_init_type() +static py::tuple +PyGlyph_get_bbox(PyGlyph *self) { - static PyMemberDef members[] = { - {(char *)"width", T_LONG, offsetof(PyGlyph, width), READONLY, (char *)""}, - {(char *)"height", T_LONG, offsetof(PyGlyph, height), READONLY, (char *)""}, - {(char *)"horiBearingX", T_LONG, offsetof(PyGlyph, horiBearingX), READONLY, (char *)""}, - {(char *)"horiBearingY", T_LONG, offsetof(PyGlyph, horiBearingY), READONLY, (char *)""}, - {(char *)"horiAdvance", T_LONG, offsetof(PyGlyph, horiAdvance), READONLY, (char *)""}, - {(char *)"linearHoriAdvance", T_LONG, offsetof(PyGlyph, linearHoriAdvance), READONLY, (char *)""}, - {(char *)"vertBearingX", T_LONG, offsetof(PyGlyph, vertBearingX), READONLY, (char *)""}, - {(char *)"vertBearingY", T_LONG, offsetof(PyGlyph, vertBearingY), READONLY, (char *)""}, - {(char *)"vertAdvance", T_LONG, offsetof(PyGlyph, vertAdvance), READONLY, (char *)""}, - {NULL} - }; - - static PyGetSetDef getset[] = { - {(char *)"bbox", (getter)PyGlyph_get_bbox, NULL, NULL, NULL}, - {NULL} - }; - - PyGlyphType.tp_name = "matplotlib.ft2font.Glyph"; - PyGlyphType.tp_basicsize = sizeof(PyGlyph); - PyGlyphType.tp_dealloc = (destructor)PyGlyph_dealloc; - PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyGlyphType.tp_members = members; - PyGlyphType.tp_getset = getset; - - return &PyGlyphType; + return py::make_tuple(self->bbox.xMin, self->bbox.yMin, + self->bbox.xMax, self->bbox.yMax); } /********************************************************************** @@ -249,40 +331,53 @@ static PyTypeObject *PyGlyph_init_type() struct PyFT2Font { - PyObject_HEAD FT2Font *x; - PyObject *py_file; + py::object py_file; FT_StreamRec stream; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; - std::vector fallbacks; + py::list fallbacks; + + ~PyFT2Font() + { + delete this->x; + } }; -static PyTypeObject PyFT2FontType; +const char *PyFT2Font__doc__ = R"""( + An object representing a single font face. + + Outside of the font itself and querying its properties, this object provides methods + for processing text strings into glyph shapes. + + Commonly, one will use `FT2Font.set_text` to load some glyph metrics and outlines. + Then `FT2Font.draw_glyphs_to_bitmap` and `FT2Font.get_image` may be used to get a + rendered form of the loaded string. -static unsigned long read_from_file_callback(FT_Stream stream, - unsigned long offset, - unsigned char *buffer, - unsigned long count) + For single characters, `FT2Font.load_char` or `FT2Font.load_glyph` may be used, + either directly for their return values, or to use `FT2Font.draw_glyph_to_bitmap` or + `FT2Font.get_path`. + + Useful metrics may be examined via the `Glyph` return values or + `FT2Font.get_kerning`. Most dimensions are given in 26.6 or 16.6 fixed-point + integers representing subpixels. Divide these values by 64 to produce floating-point + pixels. +)"""; + +static unsigned long +read_from_file_callback(FT_Stream stream, unsigned long offset, unsigned char *buffer, + unsigned long count) { - PyObject *py_file = ((PyFT2Font *)stream->descriptor.pointer)->py_file; - PyObject *seek_result = NULL, *read_result = NULL; + PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; Py_ssize_t n_read = 0; - if (!(seek_result = PyObject_CallMethod(py_file, "seek", "k", offset)) - || !(read_result = PyObject_CallMethod(py_file, "read", "k", count))) { - goto exit; - } - char *tmpbuf; - if (PyBytes_AsStringAndSize(read_result, &tmpbuf, &n_read) == -1) { - goto exit; - } - memcpy(buffer, tmpbuf, n_read); -exit: - Py_XDECREF(seek_result); - Py_XDECREF(read_result); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable(py_file); + try { + char *tmpbuf; + auto seek_result = self->py_file.attr("seek")(offset); + auto read_result = self->py_file.attr("read")(count); + if (PyBytes_AsStringAndSize(read_result.ptr(), &tmpbuf, &n_read) == -1) { + throw py::error_already_set(); + } + memcpy(buffer, tmpbuf, n_read); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); if (!count) { return 1; // Non-zero signals error, when count == 0. } @@ -290,1284 +385,1426 @@ static unsigned long read_from_file_callback(FT_Stream stream, return (unsigned long)n_read; } -static void close_file_callback(FT_Stream stream) +static void +close_file_callback(FT_Stream stream) { PyObject *type, *value, *traceback; PyErr_Fetch(&type, &value, &traceback); PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; - PyObject *close_result = NULL; - if (!(close_result = PyObject_CallMethod(self->py_file, "close", ""))) { - goto exit; - } -exit: - Py_XDECREF(close_result); - Py_CLEAR(self->py_file); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable((PyObject*)self); + try { + self->py_file.attr("close")(); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); } + self->py_file = py::object(); PyErr_Restore(type, value, traceback); } -static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +static void +ft_glyph_warn(FT_ULong charcode, std::set family_names) { - PyFT2Font *self; - self = (PyFT2Font *)type->tp_alloc(type, 0); - self->x = NULL; - self->py_file = NULL; - memset(&self->stream, 0, sizeof(FT_StreamRec)); - return (PyObject *)self; + std::set::iterator it = family_names.begin(); + std::stringstream ss; + ss<<*it; + while(++it != family_names.end()){ + ss<<", "<<*it; + } + + auto text_helpers = py::module_::import("matplotlib._text_helpers"); + auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); + warn_on_missing_glyph(charcode, ss.str()); } -const char *PyFT2Font_init__doc__ = - "FT2Font(filename, hinting_factor=8, *, _fallback_list=None, _kerning_factor=0)\n" - "--\n\n" - "Create a new FT2Font object.\n" - "\n" - "Parameters\n" - "----------\n" - "filename : str or file-like\n" - " The source of the font data in a format (ttf or ttc) that FreeType can read\n" - "\n" - "hinting_factor : int, optional\n" - " Must be positive. Used to scale the hinting in the x-direction\n" - "_fallback_list : list of FT2Font, optional\n" - " A list of FT2Font objects used to find missing glyphs.\n" - "\n" - " .. warning::\n" - " This API is both private and provisional: do not use it directly\n" - "\n" - "_kerning_factor : int, optional\n" - " Used to adjust the degree of kerning.\n" - "\n" - " .. warning::\n" - " This API is private: do not use it directly\n" - "\n" - "Attributes\n" - "----------\n" - "num_faces : int\n" - " Number of faces in file.\n" - "face_flags, style_flags : int\n" - " Face and style flags; see the ft2font constants.\n" - "num_glyphs : int\n" - " Number of glyphs in the face.\n" - "family_name, style_name : str\n" - " Face family and style name.\n" - "num_fixed_sizes : int\n" - " Number of bitmap in the face.\n" - "scalable : bool\n" - " Whether face is scalable; attributes after this one are only\n" - " defined for scalable faces.\n" - "bbox : tuple[int, int, int, int]\n" - " Face global bounding box (xmin, ymin, xmax, ymax).\n" - "units_per_EM : int\n" - " Number of font units covered by the EM.\n" - "ascender, descender : int\n" - " Ascender and descender in 26.6 units.\n" - "height : int\n" - " Height in 26.6 units; used to compute a default line spacing\n" - " (baseline-to-baseline distance).\n" - "max_advance_width, max_advance_height : int\n" - " Maximum horizontal and vertical cursor advance for all glyphs.\n" - "underline_position, underline_thickness : int\n" - " Vertical position and thickness of the underline bar.\n" - "postscript_name : str\n" - " PostScript name of the font.\n"; - -static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) +const char *PyFT2Font_init__doc__ = R"""( + Parameters + ---------- + filename : str or file-like + The source of the font data in a format (ttf or ttc) that FreeType can read. + + hinting_factor : int, optional + Must be positive. Used to scale the hinting in the x-direction. + + _fallback_list : list of FT2Font, optional + A list of FT2Font objects used to find missing glyphs. + + .. warning:: + This API is both private and provisional: do not use it directly. + + _kerning_factor : int, optional + Used to adjust the degree of kerning. + + .. warning:: + This API is private: do not use it directly. +)"""; + +static PyFT2Font * +PyFT2Font_init(py::object filename, long hinting_factor = 8, + std::optional> fallback_list = std::nullopt, + int kerning_factor = 0) { - PyObject *filename = NULL, *open = NULL, *data = NULL, *fallback_list = NULL; - FT_Open_Args open_args; - long hinting_factor = 8; - int kerning_factor = 0; - const char *names[] = { - "filename", "hinting_factor", "_fallback_list", "_kerning_factor", NULL - }; - std::vector fallback_fonts; - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|l$Oi:FT2Font", (char **)names, &filename, - &hinting_factor, &fallback_list, &kerning_factor)) { - return -1; - } if (hinting_factor <= 0) { - PyErr_SetString(PyExc_ValueError, - "hinting_factor must be greater than 0"); - goto exit; + throw py::value_error("hinting_factor must be greater than 0"); } + PyFT2Font *self = new PyFT2Font(); + self->x = NULL; + memset(&self->stream, 0, sizeof(FT_StreamRec)); self->stream.base = NULL; self->stream.size = 0x7fffffff; // Unknown size. self->stream.pos = 0; self->stream.descriptor.pointer = self; self->stream.read = &read_from_file_callback; + FT_Open_Args open_args; memset((void *)&open_args, 0, sizeof(FT_Open_Args)); open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; + std::vector fallback_fonts; if (fallback_list) { - if (!PyList_Check(fallback_list)) { - PyErr_SetString(PyExc_TypeError, "Fallback list must be a list"); - goto exit; - } - Py_ssize_t size = PyList_Size(fallback_list); - - // go through fallbacks once to make sure the types are right - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - if (!PyObject_IsInstance(item, PyObject_Type(reinterpret_cast(self)))) { - PyErr_SetString(PyExc_TypeError, "Fallback fonts must be FT2Font objects."); - goto exit; - } - } - // go through a second time to add them to our lists - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - // Increase the ref count, we will undo this in dealloc this makes - // sure things do not get gc'd under us! - Py_INCREF(item); - self->fallbacks.push_back(item); + // go through fallbacks to add them to our lists + for (auto item : *fallback_list) { + self->fallbacks.append(item); // Also (locally) cache the underlying FT2Font objects. As long as // the Python objects are kept alive, these pointer are good. - FT2Font *fback = reinterpret_cast(item)->x; + FT2Font *fback = item->x; fallback_fonts.push_back(fback); } } - if (PyBytes_Check(filename) || PyUnicode_Check(filename)) { - if (!(open = PyDict_GetItemString(PyEval_GetBuiltins(), "open")) // Borrowed reference. - || !(self->py_file = PyObject_CallFunction(open, "Os", filename, "rb"))) { - goto exit; - } + if (py::isinstance(filename) || py::isinstance(filename)) { + self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; - } else if (!PyObject_HasAttrString(filename, "read") - || !(data = PyObject_CallMethod(filename, "read", "i", 0)) - || !PyBytes_Check(data)) { - PyErr_SetString(PyExc_TypeError, - "First argument must be a path to a font file or a binary-mode file object"); - Py_CLEAR(data); - goto exit; } else { + try { + // This will catch various issues: + // 1. `read` not being an attribute. + // 2. `read` raising an error. + // 3. `read` returning something other than `bytes`. + auto data = filename.attr("read")(0).cast(); + } catch (const std::exception&) { + throw py::type_error( + "First argument must be a path to a font file or a binary-mode file object"); + } self->py_file = filename; self->stream.close = NULL; - Py_INCREF(filename); } - Py_CLEAR(data); - CALL_CPP_FULL( - "FT2Font", (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts)), - Py_CLEAR(self->py_file), -1); + self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn); - CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); + self->x->set_kerning_factor(kerning_factor); -exit: - return PyErr_Occurred() ? -1 : 0; + return self; } -static void PyFT2Font_dealloc(PyFT2Font *self) -{ - delete self->x; - for (size_t i = 0; i < self->fallbacks.size(); i++) { - Py_DECREF(self->fallbacks[i]); - } +const char *PyFT2Font_clear__doc__ = + "Clear all the glyphs, reset for a new call to `.set_text`."; - Py_XDECREF(self->py_file); - Py_TYPE(self)->tp_free((PyObject *)self); +static void +PyFT2Font_clear(PyFT2Font *self) +{ + self->x->clear(); } -const char *PyFT2Font_clear__doc__ = - "clear(self)\n" - "--\n\n" - "Clear all the glyphs, reset for a new call to `.set_text`.\n"; +const char *PyFT2Font_set_size__doc__ = R"""( + Set the size of the text. -static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args) -{ - CALL_CPP("clear", (self->x->clear())); + Parameters + ---------- + ptsize : float + The size of the text in points. + dpi : float + The DPI used for rendering the text. +)"""; - Py_RETURN_NONE; +static void +PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) +{ + self->x->set_size(ptsize, dpi); } -const char *PyFT2Font_set_size__doc__ = - "set_size(self, ptsize, dpi)\n" - "--\n\n" - "Set the point size and dpi of the text.\n"; +const char *PyFT2Font_set_charmap__doc__ = R"""( + Make the i-th charmap current. -static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args) -{ - double ptsize; - double dpi; + For more details on character mapping, see the `FreeType documentation + `_. - if (!PyArg_ParseTuple(args, "dd:set_size", &ptsize, &dpi)) { - return NULL; - } + Parameters + ---------- + i : int + The charmap number in the range [0, `.num_charmaps`). - CALL_CPP("set_size", (self->x->set_size(ptsize, dpi))); + See Also + -------- + .num_charmaps + .select_charmap + .get_charmap +)"""; - Py_RETURN_NONE; +static void +PyFT2Font_set_charmap(PyFT2Font *self, int i) +{ + self->x->set_charmap(i); } -const char *PyFT2Font_set_charmap__doc__ = - "set_charmap(self, i)\n" - "--\n\n" - "Make the i-th charmap current.\n"; +const char *PyFT2Font_select_charmap__doc__ = R"""( + Select a charmap by its FT_Encoding number. -static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args) -{ - int i; + For more details on character mapping, see the `FreeType documentation + `_. - if (!PyArg_ParseTuple(args, "i:set_charmap", &i)) { - return NULL; - } + Parameters + ---------- + i : int + The charmap in the form defined by FreeType: + https://freetype.org/freetype2/docs/reference/ft2-character_mapping.html#ft_encoding - CALL_CPP("set_charmap", (self->x->set_charmap(i))); + See Also + -------- + .set_charmap + .get_charmap +)"""; - Py_RETURN_NONE; +static void +PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) +{ + self->x->select_charmap(i); } -const char *PyFT2Font_select_charmap__doc__ = - "select_charmap(self, i)\n" - "--\n\n" - "Select a charmap by its FT_Encoding number.\n"; +const char *PyFT2Font_get_kerning__doc__ = R"""( + Get the kerning between two glyphs. -static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args) -{ - unsigned long i; + Parameters + ---------- + left, right : int + The glyph indices. Note these are not characters nor character codes. + Use `.get_char_index` to convert character codes to glyph indices. - if (!PyArg_ParseTuple(args, "k:select_charmap", &i)) { - return NULL; - } + mode : Kerning + A kerning mode constant: - CALL_CPP("select_charmap", self->x->select_charmap(i)); + - ``DEFAULT`` - Return scaled and grid-fitted kerning distances. + - ``UNFITTED`` - Return scaled but un-grid-fitted kerning distances. + - ``UNSCALED`` - Return the kerning vector in original font units. - Py_RETURN_NONE; -} + .. versionchanged:: 3.10 + This now takes a `.ft2font.Kerning` value instead of an `int`. -const char *PyFT2Font_get_kerning__doc__ = - "get_kerning(self, left, right, mode)\n" - "--\n\n" - "Get the kerning between *left* and *right* glyph indices.\n" - "*mode* is a kerning mode constant:\n\n" - "- KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" - "- KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" - "- KERNING_UNSCALED - Return the kerning vector in original font units\n"; + Returns + ------- + int + The kerning adjustment between the two glyphs. +)"""; -static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) +static int +PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, + std::variant mode_or_int) { - FT_UInt left, right, mode; - int result; - int fallback = 1; + bool fallback = true; + FT_Kerning_Mode mode; - if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { - return NULL; + if (auto value = std::get_if(&mode_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="mode", "obj_type"_a="parameter as int", + "alternative"_a="Kerning enum values"); + mode = static_cast(*value); + } else if (auto value = std::get_if(&mode_or_int)) { + mode = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("mode must be Kerning or int"); } - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, (bool)fallback))); - - return PyLong_FromLong(result); + return self->x->get_kerning(left, right, mode, fallback); } -const char *PyFT2Font_get_fontmap__doc__ = - "_get_fontmap(self, string)\n" - "--\n\n" - "Get a mapping between characters and the font that includes them.\n" - "A dictionary mapping unicode characters to PyFT2Font objects."; -static PyObject *PyFT2Font_get_fontmap(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - PyObject *textobj; - const char *names[] = { "string", NULL }; +const char *PyFT2Font_get_fontmap__doc__ = R"""( + Get a mapping between characters and the font that includes them. - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O:_get_fontmap", (char **)names, &textobj)) { - return NULL; - } + .. warning:: + This API uses the fallback list and is both private and provisional: do not use + it directly. + Parameters + ---------- + text : str + The characters for which to find fonts. + + Returns + ------- + dict[str, FT2Font] + A dictionary mapping unicode characters to `.FT2Font` objects. +)"""; + +static py::dict +PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) +{ std::set codepoints; - size_t size; - if (PyUnicode_Check(textobj)) { - size = PyUnicode_GET_LENGTH(textobj); - for (size_t i = 0; i < size; ++i) { - codepoints.insert(PyUnicode_ReadChar(textobj, i)); + py::dict char_to_font; + for (auto code : text) { + if (!codepoints.insert(code).second) { + continue; } - } else { - PyErr_SetString(PyExc_TypeError, "string must be str"); - return NULL; - } - PyObject *char_to_font; - if (!(char_to_font = PyDict_New())) { - return NULL; - } - for (auto it = codepoints.begin(); it != codepoints.end(); ++it) { - auto x = *it; - PyObject* target_font; + + py::object target_font; int index; - if (self->x->get_char_fallback_index(x, index)) { + if (self->x->get_char_fallback_index(code, index)) { if (index >= 0) { target_font = self->fallbacks[index]; } else { - target_font = (PyObject *)self; + target_font = py::cast(self); } } else { // TODO Handle recursion! - target_font = (PyObject *)self; + target_font = py::cast(self); } - PyObject *key = NULL; - bool error = (!(key = PyUnicode_FromFormat("%c", x)) - || (PyDict_SetItem(char_to_font, key, target_font) == -1)); - Py_XDECREF(key); - if (error) { - Py_DECREF(char_to_font); - PyErr_SetString(PyExc_ValueError, "Something went very wrong"); - return NULL; - } + auto key = py::cast(std::u32string(1, code)); + char_to_font[key] = target_font; } return char_to_font; } +const char *PyFT2Font_set_text__doc__ = R"""( + Set the text *string* and *angle*. -const char *PyFT2Font_set_text__doc__ = - "set_text(self, string, angle=0.0, flags=32)\n" - "--\n\n" - "Set the text *string* and *angle*.\n" - "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" - "the default value is LOAD_FORCE_AUTOHINT.\n" - "You must call this before `.draw_glyphs_to_bitmap`.\n" - "A sequence of x,y positions is returned.\n"; + You must call this before `.draw_glyphs_to_bitmap`. -static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - PyObject *textobj; - double angle = 0.0; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; - std::vector xys; - const char *names[] = { "string", "angle", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|di:set_text", (char **)names, &textobj, &angle, &flags)) { - return NULL; - } + Parameters + ---------- + string : str + The text to prepare rendering information for. + angle : float + The angle at which to render the supplied text. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. - std::vector codepoints; - size_t size; + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. - if (PyUnicode_Check(textobj)) { - size = PyUnicode_GET_LENGTH(textobj); - codepoints.resize(size); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = PyUnicode_ReadChar(textobj, i); - } + Returns + ------- + np.ndarray[double] + A sequence of x,y glyph positions in 26.6 subpixels; divide by 64 for pixels. +)"""; + +static py::array_t +PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) +{ + std::vector xys; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; } else { - PyErr_SetString(PyExc_TypeError, "set_text requires str-input."); - return NULL; + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); } - uint32_t* codepoints_array = NULL; - if (size > 0) { - codepoints_array = &codepoints[0]; - } - CALL_CPP("set_text", self->x->set_text(size, codepoints_array, angle, flags, xys)); + self->x->set_text(text, angle, static_cast(flags), xys); - return convert_xys_to_array(xys); + py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; + py::array_t result(dims); + if (xys.size() > 0) { + memcpy(result.mutable_data(), xys.data(), result.nbytes()); + } + return result; } -const char *PyFT2Font_get_num_glyphs__doc__ = - "get_num_glyphs(self)\n" - "--\n\n" - "Return the number of loaded glyphs.\n"; +const char *PyFT2Font_get_num_glyphs__doc__ = "Return the number of loaded glyphs."; -static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args) +static size_t +PyFT2Font_get_num_glyphs(PyFT2Font *self) { - return PyLong_FromSize_t(self->x->get_num_glyphs()); + return self->x->get_num_glyphs(); } -const char *PyFT2Font_load_char__doc__ = - "load_char(self, charcode, flags=32)\n" - "--\n\n" - "Load character with *charcode* in current fontfile and set glyph.\n" - "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" - "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n\n" - "- width: glyph width\n" - "- height: glyph height\n" - "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" - "- horiBearingX: left side bearing in horizontal layouts\n" - "- horiBearingY: top side bearing in horizontal layouts\n" - "- horiAdvance: advance width for horizontal layout\n" - "- vertBearingX: left side bearing in vertical layouts\n" - "- vertBearingY: top side bearing in vertical layouts\n" - "- vertAdvance: advance height for vertical layout\n"; - -static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) +const char *PyFT2Font_load_char__doc__ = R"""( + Load character in current fontfile and set glyph. + + Parameters + ---------- + charcode : int + The character code to prepare rendering information for. This code must be in + the charmap, or else a ``.notdef`` glyph may be returned instead. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. + + Returns + ------- + Glyph + The glyph information corresponding to the specified character. + + See Also + -------- + .load_glyph + .select_charmap + .set_charmap +)"""; + +static PyGlyph * +PyFT2Font_load_char(PyFT2Font *self, long charcode, + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { - long charcode; - int fallback = 1; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; - const char *names[] = { "charcode", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|i:load_char", (char **)names, &charcode, - &flags)) { - return NULL; + bool fallback = true; + FT2Font *ft_object = NULL; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); } - FT2Font *ft_object = NULL; - CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, (bool)fallback))); + self->x->load_char(charcode, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } -const char *PyFT2Font_load_glyph__doc__ = - "load_glyph(self, glyphindex, flags=32)\n" - "--\n\n" - "Load character with *glyphindex* in current fontfile and set glyph.\n" - "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" - "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n\n" - "- width: glyph width\n" - "- height: glyph height\n" - "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" - "- horiBearingX: left side bearing in horizontal layouts\n" - "- horiBearingY: top side bearing in horizontal layouts\n" - "- horiAdvance: advance width for horizontal layout\n" - "- vertBearingX: left side bearing in vertical layouts\n" - "- vertBearingY: top side bearing in vertical layouts\n" - "- vertAdvance: advance height for vertical layout\n"; - -static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject *kwds) +const char *PyFT2Font_load_glyph__doc__ = R"""( + Load glyph index in current fontfile and set glyph. + + Note that the glyph index is specific to a font, and not universal like a Unicode + code point. + + Parameters + ---------- + glyph_index : int + The glyph index to prepare rendering information for. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. + + Returns + ------- + Glyph + The glyph information corresponding to the specified index. + + See Also + -------- + .load_char +)"""; + +static PyGlyph * +PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { - FT_UInt glyph_index; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; - int fallback = 1; - const char *names[] = { "glyph_index", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, - &flags)) { - return NULL; + bool fallback = true; + FT2Font *ft_object = NULL; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); } - FT2Font *ft_object = NULL; - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, (bool)fallback))); + self->x->load_glyph(glyph_index, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } -const char *PyFT2Font_get_width_height__doc__ = - "get_width_height(self)\n" - "--\n\n" - "Get the width and height in 26.6 subpixels of the current string set by `.set_text`.\n" - "The rotation of the string is accounted for. To get width and height\n" - "in pixels, divide these values by 64.\n"; +const char *PyFT2Font_get_width_height__doc__ = R"""( + Get the dimensions of the current string set by `.set_text`. + + The rotation of the string is accounted for. -static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args) + Returns + ------- + width, height : float + The width and height in 26.6 subpixels of the current string. To get width and + height in pixels, divide these values by 64. + + See Also + -------- + .get_bitmap_offset + .get_descent +)"""; + +static py::tuple +PyFT2Font_get_width_height(PyFT2Font *self) { long width, height; - CALL_CPP("get_width_height", (self->x->get_width_height(&width, &height))); + self->x->get_width_height(&width, &height); - return Py_BuildValue("ll", width, height); + return py::make_tuple(width, height); } -const char *PyFT2Font_get_bitmap_offset__doc__ = - "get_bitmap_offset(self)\n" - "--\n\n" - "Get the (x, y) offset in 26.6 subpixels for the bitmap if ink hangs left or below (0, 0).\n" - "Since Matplotlib only supports left-to-right text, y is always 0.\n"; +const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( + Get the (x, y) offset for the bitmap if ink hangs left or below (0, 0). + + Since Matplotlib only supports left-to-right text, y is always 0. -static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args) + Returns + ------- + x, y : float + The x and y offset in 26.6 subpixels of the bitmap. To get x and y in pixels, + divide these values by 64. + + See Also + -------- + .get_width_height + .get_descent +)"""; + +static py::tuple +PyFT2Font_get_bitmap_offset(PyFT2Font *self) { long x, y; - CALL_CPP("get_bitmap_offset", (self->x->get_bitmap_offset(&x, &y))); + self->x->get_bitmap_offset(&x, &y); - return Py_BuildValue("ll", x, y); + return py::make_tuple(x, y); } -const char *PyFT2Font_get_descent__doc__ = - "get_descent(self)\n" - "--\n\n" - "Get the descent in 26.6 subpixels of the current string set by `.set_text`.\n" - "The rotation of the string is accounted for. To get the descent\n" - "in pixels, divide this value by 64.\n"; +const char *PyFT2Font_get_descent__doc__ = R"""( + Get the descent of the current string set by `.set_text`. -static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args) -{ - long descent; + The rotation of the string is accounted for. + + Returns + ------- + int + The descent in 26.6 subpixels of the bitmap. To get the descent in pixels, + divide these values by 64. - CALL_CPP("get_descent", (descent = self->x->get_descent())); + See Also + -------- + .get_bitmap_offset + .get_width_height +)"""; - return PyLong_FromLong(descent); +static long +PyFT2Font_get_descent(PyFT2Font *self) +{ + return self->x->get_descent(); } -const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = - "draw_glyphs_to_bitmap(self, antialiased=True)\n" - "--\n\n" - "Draw the glyphs that were loaded by `.set_text` to the bitmap.\n" - "The bitmap size will be automatically set to include the glyphs.\n"; +const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( + Draw the glyphs that were loaded by `.set_text` to the bitmap. -static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - bool antialiased = true; - const char *names[] = { "antialiased", NULL }; + The bitmap size will be automatically set to include the glyphs. - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&:draw_glyphs_to_bitmap", - (char **)names, &convert_bool, &antialiased)) { - return NULL; - } + Parameters + ---------- + antialiased : bool, default: True + Whether to render glyphs 8-bit antialiased or in pure black-and-white. - CALL_CPP("draw_glyphs_to_bitmap", (self->x->draw_glyphs_to_bitmap(antialiased))); + See Also + -------- + .draw_glyph_to_bitmap +)"""; - Py_RETURN_NONE; +static void +PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) +{ + self->x->draw_glyphs_to_bitmap(antialiased); } -const char *PyFT2Font_get_xys__doc__ = - "get_xys(self, antialiased=True)\n" - "--\n\n" - "Get the xy locations of the current glyphs.\n" - "\n" - ".. deprecated:: 3.8\n"; +const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( + Draw a single glyph to the bitmap at pixel locations x, y. -static PyObject *PyFT2Font_get_xys(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - char const* msg = - "FT2Font.get_xys is deprecated since Matplotlib 3.8 and will be removed in " - "Matplotlib 3.10 as it is not used in the library. If you rely on it, " - "please let us know."; - if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { - return NULL; - } + Note it is your responsibility to create the image manually with the correct size + before this call is made. - bool antialiased = true; - std::vector xys; - const char *names[] = { "antialiased", NULL }; + If you want automatic layout, use `.set_text` in combinations with + `.draw_glyphs_to_bitmap`. This function is instead intended for people who want to + render individual glyphs (e.g., returned by `.load_char`) at precise locations. - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&:get_xys", - (char **)names, &convert_bool, &antialiased)) { - return NULL; - } + Parameters + ---------- + image : FT2Image + The image buffer on which to draw the glyph. + x, y : int + The pixel location at which to draw the glyph. + glyph : Glyph + The glyph to draw. + antialiased : bool, default: True + Whether to render glyphs 8-bit antialiased or in pure black-and-white. + + See Also + -------- + .draw_glyphs_to_bitmap +)"""; - CALL_CPP("get_xys", (self->x->get_xys(antialiased, xys))); +static void +PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, + double_or_ vxd, double_or_ vyd, + PyGlyph *glyph, bool antialiased = true) +{ + auto xd = _double_to_("x", vxd); + auto yd = _double_to_("y", vyd); - return convert_xys_to_array(xys); + self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased); } -const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = - "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" - "--\n\n" - "Draw a single glyph to the bitmap at pixel locations x, y\n" - "Note it is your responsibility to set up the bitmap manually\n" - "with ``set_bitmap_size(w, h)`` before this call is made.\n" - "\n" - "If you want automatic layout, use `.set_text` in combinations with\n" - "`.draw_glyphs_to_bitmap`. This function is instead intended for people\n" - "who want to render individual glyphs (e.g., returned by `.load_char`)\n" - "at precise locations.\n"; - -static PyObject *PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - PyFT2Image *image; - double xd, yd; - PyGlyph *glyph; - bool antialiased = true; - const char *names[] = { "image", "x", "y", "glyph", "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, - kwds, - "O!ddO!|O&:draw_glyph_to_bitmap", - (char **)names, - &PyFT2ImageType, - &image, - &xd, - &yd, - &PyGlyphType, - &glyph, - &convert_bool, - &antialiased)) { - return NULL; - } +const char *PyFT2Font_get_glyph_name__doc__ = R"""( + Retrieve the ASCII name of a given glyph *index* in a face. - CALL_CPP("draw_glyph_to_bitmap", - self->x->draw_glyph_to_bitmap(*(image->x), xd, yd, glyph->glyphInd, antialiased)); + Due to Matplotlib's internal design, for fonts that do not contain glyph names (per + ``FT_FACE_FLAG_GLYPH_NAMES``), this returns a made-up name which does *not* + roundtrip through `.get_name_index`. - Py_RETURN_NONE; -} + Parameters + ---------- + index : int + The glyph number to query. + + Returns + ------- + str + The name of the glyph, or if the font does not contain names, a name synthesized + by Matplotlib. -const char *PyFT2Font_get_glyph_name__doc__ = - "get_glyph_name(self, index)\n" - "--\n\n" - "Retrieve the ASCII name of a given glyph *index* in a face.\n" - "\n" - "Due to Matplotlib's internal design, for fonts that do not contain glyph\n" - "names (per FT_FACE_FLAG_GLYPH_NAMES), this returns a made-up name which\n" - "does *not* roundtrip through `.get_name_index`.\n"; + See Also + -------- + .get_name_index +)"""; -static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) +static py::str +PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) { - unsigned int glyph_number; - char buffer[128]; - int fallback = 1; + std::string buffer; + bool fallback = true; - if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { - return NULL; - } - CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer, (bool)fallback))); - return PyUnicode_FromString(buffer); + buffer.resize(128); + self->x->get_glyph_name(glyph_number, buffer, fallback); + return buffer; } -const char *PyFT2Font_get_charmap__doc__ = - "get_charmap(self)\n" - "--\n\n" - "Return a dict that maps the character codes of the selected charmap\n" - "(Unicode by default) to their corresponding glyph indices.\n"; +const char *PyFT2Font_get_charmap__doc__ = R"""( + Return a mapping of character codes to glyph indices in the font. + + The charmap is Unicode by default, but may be changed by `.set_charmap` or + `.select_charmap`. + + Returns + ------- + dict[int, int] + A dictionary of the selected charmap mapping character codes to their + corresponding glyph indices. +)"""; -static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) +static py::dict +PyFT2Font_get_charmap(PyFT2Font *self) { - PyObject *charmap; - if (!(charmap = PyDict_New())) { - return NULL; - } + py::dict charmap; FT_UInt index; FT_ULong code = FT_Get_First_Char(self->x->get_face(), &index); while (index != 0) { - PyObject *key = NULL, *val = NULL; - bool error = (!(key = PyLong_FromLong(code)) - || !(val = PyLong_FromLong(index)) - || (PyDict_SetItem(charmap, key, val) == -1)); - Py_XDECREF(key); - Py_XDECREF(val); - if (error) { - Py_DECREF(charmap); - return NULL; - } + charmap[py::cast(code)] = py::cast(index); code = FT_Get_Next_Char(self->x->get_face(), code, &index); } return charmap; } +const char *PyFT2Font_get_char_index__doc__ = R"""( + Return the glyph index corresponding to a character code point. -const char *PyFT2Font_get_char_index__doc__ = - "get_char_index(self, codepoint)\n" - "--\n\n" - "Return the glyph index corresponding to a character *codepoint*.\n"; + Parameters + ---------- + codepoint : int + A character code point in the current charmap (which defaults to Unicode.) -static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) -{ - FT_UInt index; - FT_ULong ccode; - int fallback = 1; + Returns + ------- + int + The corresponding glyph index. - if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { - return NULL; - } + See Also + -------- + .set_charmap + .select_charmap + .get_glyph_name + .get_name_index +)"""; - CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, (bool)fallback)); +static FT_UInt +PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) +{ + bool fallback = true; - return PyLong_FromLong(index); + return self->x->get_char_index(ccode, fallback); } +const char *PyFT2Font_get_sfnt__doc__ = R"""( + Load the entire SFNT names table. -const char *PyFT2Font_get_sfnt__doc__ = - "get_sfnt(self)\n" - "--\n\n" - "Load the entire SFNT names table, as a dict whose keys are\n" - "(platform-ID, ISO-encoding-scheme, language-code, and description)\n" - "tuples.\n"; + Returns + ------- + dict[tuple[int, int, int, int], bytes] + The SFNT names table; the dictionary keys are tuples of: -static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args) -{ - PyObject *names; + (platform-ID, ISO-encoding-scheme, language-code, description) + and the values are the direct information from the font table. +)"""; + +static py::dict +PyFT2Font_get_sfnt(PyFT2Font *self) +{ if (!(self->x->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { - PyErr_SetString(PyExc_ValueError, "No SFNT name table"); - return NULL; + throw py::value_error("No SFNT name table"); } size_t count = FT_Get_Sfnt_Name_Count(self->x->get_face()); - names = PyDict_New(); - if (names == NULL) { - return NULL; - } + py::dict names; for (FT_UInt j = 0; j < count; ++j) { FT_SfntName sfnt; FT_Error error = FT_Get_Sfnt_Name(self->x->get_face(), j, &sfnt); if (error) { - Py_DECREF(names); - PyErr_SetString(PyExc_ValueError, "Could not get SFNT name"); - return NULL; - } - - PyObject *key = Py_BuildValue( - "HHHH", sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); - if (key == NULL) { - Py_DECREF(names); - return NULL; - } - - PyObject *val = PyBytes_FromStringAndSize((const char *)sfnt.string, sfnt.string_len); - if (val == NULL) { - Py_DECREF(key); - Py_DECREF(names); - return NULL; + throw py::value_error("Could not get SFNT name"); } - if (PyDict_SetItem(names, key, val)) { - Py_DECREF(key); - Py_DECREF(val); - Py_DECREF(names); - return NULL; - } - - Py_DECREF(key); - Py_DECREF(val); + auto key = py::make_tuple( + sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); + auto val = py::bytes(reinterpret_cast(sfnt.string), + sfnt.string_len); + names[key] = val; } return names; } -const char *PyFT2Font_get_name_index__doc__ = - "get_name_index(self, name)\n" - "--\n\n" - "Return the glyph index of a given glyph *name*.\n" - "The glyph index 0 means 'undefined character code'.\n"; +const char *PyFT2Font_get_name_index__doc__ = R"""( + Return the glyph index of a given glyph *name*. + + Parameters + ---------- + name : str + The name of the glyph to query. + + Returns + ------- + int + The corresponding glyph index; 0 means 'undefined character code'. -static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args) + See Also + -------- + .get_char_index + .get_glyph_name +)"""; + +static long +PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) { - char *glyphname; - long name_index; - if (!PyArg_ParseTuple(args, "s:get_name_index", &glyphname)) { - return NULL; - } - CALL_CPP("get_name_index", name_index = self->x->get_name_index(glyphname)); - return PyLong_FromLong(name_index); + return self->x->get_name_index(glyphname); } -const char *PyFT2Font_get_ps_font_info__doc__ = - "get_ps_font_info(self)\n" - "--\n\n" - "Return the information in the PS Font Info structure.\n"; +const char *PyFT2Font_get_ps_font_info__doc__ = R"""( + Return the information in the PS Font Info structure. + + For more information, see the `FreeType documentation on this structure + `_. -static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args) + Returns + ------- + version : str + notice : str + full_name : str + family_name : str + weight : str + italic_angle : int + is_fixed_pitch : bool + underline_position : int + underline_thickness : int +)"""; + +static py::tuple +PyFT2Font_get_ps_font_info(PyFT2Font *self) { PS_FontInfoRec fontinfo; FT_Error error = FT_Get_PS_Font_Info(self->x->get_face(), &fontinfo); if (error) { - PyErr_SetString(PyExc_ValueError, "Could not get PS font info"); - return NULL; - } - - return Py_BuildValue("ssssslbhH", - fontinfo.version ? fontinfo.version : "", - fontinfo.notice ? fontinfo.notice : "", - fontinfo.full_name ? fontinfo.full_name : "", - fontinfo.family_name ? fontinfo.family_name : "", - fontinfo.weight ? fontinfo.weight : "", - fontinfo.italic_angle, - fontinfo.is_fixed_pitch, - fontinfo.underline_position, - fontinfo.underline_thickness); -} - -const char *PyFT2Font_get_sfnt_table__doc__ = - "get_sfnt_table(self, name)\n" - "--\n\n" - "Return one of the following SFNT tables: head, maxp, OS/2, hhea, " - "vhea, post, or pclt.\n"; - -static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) -{ - char *tagname; - if (!PyArg_ParseTuple(args, "s:get_sfnt_table", &tagname)) { - return NULL; + throw py::value_error("Could not get PS font info"); } - int tag; - const char *tags[] = { "head", "maxp", "OS/2", "hhea", "vhea", "post", "pclt", NULL }; + return py::make_tuple( + fontinfo.version ? fontinfo.version : "", + fontinfo.notice ? fontinfo.notice : "", + fontinfo.full_name ? fontinfo.full_name : "", + fontinfo.family_name ? fontinfo.family_name : "", + fontinfo.weight ? fontinfo.weight : "", + fontinfo.italic_angle, + fontinfo.is_fixed_pitch, + fontinfo.underline_position, + fontinfo.underline_thickness); +} + +const char *PyFT2Font_get_sfnt_table__doc__ = R"""( + Return one of the SFNT tables. + + Parameters + ---------- + name : {"head", "maxp", "OS/2", "hhea", "vhea", "post", "pclt"} + Which table to return. + + Returns + ------- + dict[str, Any] + The corresponding table; for more information, see `the FreeType documentation + `_. +)"""; + +static std::optional +PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) +{ + FT_Sfnt_Tag tag; + const std::unordered_map names = { + {"head", FT_SFNT_HEAD}, + {"maxp", FT_SFNT_MAXP}, + {"OS/2", FT_SFNT_OS2}, + {"hhea", FT_SFNT_HHEA}, + {"vhea", FT_SFNT_VHEA}, + {"post", FT_SFNT_POST}, + {"pclt", FT_SFNT_PCLT}, + }; - for (tag = 0; tags[tag] != NULL; tag++) { - if (strncmp(tagname, tags[tag], 5) == 0) { - break; - } + try { + tag = names.at(tagname); + } catch (const std::out_of_range&) { + return std::nullopt; } - void *table = FT_Get_Sfnt_Table(self->x->get_face(), (FT_Sfnt_Tag)tag); + void *table = FT_Get_Sfnt_Table(self->x->get_face(), tag); if (!table) { - Py_RETURN_NONE; + return std::nullopt; } switch (tag) { - case 0: { - char head_dict[] = - "{s:(h,H), s:(h,H), s:l, s:l, s:H, s:H," - "s:(l,l), s:(l,l), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; - TT_Header *t = (TT_Header *)table; - return Py_BuildValue(head_dict, - "version", FIXED_MAJOR(t->Table_Version), FIXED_MINOR(t->Table_Version), - "fontRevision", FIXED_MAJOR(t->Font_Revision), FIXED_MINOR(t->Font_Revision), - "checkSumAdjustment", t->CheckSum_Adjust, - "magicNumber", t->Magic_Number, - "flags", t->Flags, - "unitsPerEm", t->Units_Per_EM, - "created", t->Created[0], t->Created[1], - "modified", t->Modified[0], t->Modified[1], - "xMin", t->xMin, - "yMin", t->yMin, - "xMax", t->xMax, - "yMax", t->yMax, - "macStyle", t->Mac_Style, - "lowestRecPPEM", t->Lowest_Rec_PPEM, - "fontDirectionHint", t->Font_Direction, - "indexToLocFormat", t->Index_To_Loc_Format, - "glyphDataFormat", t->Glyph_Data_Format); + case FT_SFNT_HEAD: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Table_Version), + FIXED_MINOR(t->Table_Version)), + "fontRevision"_a=py::make_tuple(FIXED_MAJOR(t->Font_Revision), + FIXED_MINOR(t->Font_Revision)), + "checkSumAdjustment"_a=t->CheckSum_Adjust, + "magicNumber"_a=t->Magic_Number, + "flags"_a=t->Flags, + "unitsPerEm"_a=t->Units_Per_EM, + // FreeType 2.6.1 defines these two timestamps as FT_Long, but they should + // be unsigned (fixed in 2.10.0): + // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 + // It's actually read from the file structure as two 32-bit values, so we + // need to cast down in size to prevent sign extension from producing huge + // 64-bit values. + "created"_a=py::make_tuple(static_cast(t->Created[0]), + static_cast(t->Created[1])), + "modified"_a=py::make_tuple(static_cast(t->Modified[0]), + static_cast(t->Modified[1])), + "xMin"_a=t->xMin, + "yMin"_a=t->yMin, + "xMax"_a=t->xMax, + "yMax"_a=t->yMax, + "macStyle"_a=t->Mac_Style, + "lowestRecPPEM"_a=t->Lowest_Rec_PPEM, + "fontDirectionHint"_a=t->Font_Direction, + "indexToLocFormat"_a=t->Index_To_Loc_Format, + "glyphDataFormat"_a=t->Glyph_Data_Format); } - case 1: { - char maxp_dict[] = - "{s:(h,H), s:H, s:H, s:H, s:H, s:H, s:H," - "s:H, s:H, s:H, s:H, s:H, s:H, s:H, s:H}"; - TT_MaxProfile *t = (TT_MaxProfile *)table; - return Py_BuildValue(maxp_dict, - "version", FIXED_MAJOR(t->version), FIXED_MINOR(t->version), - "numGlyphs", t->numGlyphs, - "maxPoints", t->maxPoints, - "maxContours", t->maxContours, - "maxComponentPoints", t->maxCompositePoints, - "maxComponentContours", t->maxCompositeContours, - "maxZones", t->maxZones, - "maxTwilightPoints", t->maxTwilightPoints, - "maxStorage", t->maxStorage, - "maxFunctionDefs", t->maxFunctionDefs, - "maxInstructionDefs", t->maxInstructionDefs, - "maxStackElements", t->maxStackElements, - "maxSizeOfInstructions", t->maxSizeOfInstructions, - "maxComponentElements", t->maxComponentElements, - "maxComponentDepth", t->maxComponentDepth); + case FT_SFNT_MAXP: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->version), + FIXED_MINOR(t->version)), + "numGlyphs"_a=t->numGlyphs, + "maxPoints"_a=t->maxPoints, + "maxContours"_a=t->maxContours, + "maxComponentPoints"_a=t->maxCompositePoints, + "maxComponentContours"_a=t->maxCompositeContours, + "maxZones"_a=t->maxZones, + "maxTwilightPoints"_a=t->maxTwilightPoints, + "maxStorage"_a=t->maxStorage, + "maxFunctionDefs"_a=t->maxFunctionDefs, + "maxInstructionDefs"_a=t->maxInstructionDefs, + "maxStackElements"_a=t->maxStackElements, + "maxSizeOfInstructions"_a=t->maxSizeOfInstructions, + "maxComponentElements"_a=t->maxComponentElements, + "maxComponentDepth"_a=t->maxComponentDepth); } - case 2: { - char os_2_dict[] = - "{s:H, s:h, s:H, s:H, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:y#, s:(kkkk)," - "s:y#, s:H, s:H, s:H}"; - TT_OS2 *t = (TT_OS2 *)table; - return Py_BuildValue(os_2_dict, - "version", t->version, - "xAvgCharWidth", t->xAvgCharWidth, - "usWeightClass", t->usWeightClass, - "usWidthClass", t->usWidthClass, - "fsType", t->fsType, - "ySubscriptXSize", t->ySubscriptXSize, - "ySubscriptYSize", t->ySubscriptYSize, - "ySubscriptXOffset", t->ySubscriptXOffset, - "ySubscriptYOffset", t->ySubscriptYOffset, - "ySuperscriptXSize", t->ySuperscriptXSize, - "ySuperscriptYSize", t->ySuperscriptYSize, - "ySuperscriptXOffset", t->ySuperscriptXOffset, - "ySuperscriptYOffset", t->ySuperscriptYOffset, - "yStrikeoutSize", t->yStrikeoutSize, - "yStrikeoutPosition", t->yStrikeoutPosition, - "sFamilyClass", t->sFamilyClass, - "panose", t->panose, Py_ssize_t(10), - "ulCharRange", t->ulUnicodeRange1, t->ulUnicodeRange2, t->ulUnicodeRange3, t->ulUnicodeRange4, - "achVendID", t->achVendID, Py_ssize_t(4), - "fsSelection", t->fsSelection, - "fsFirstCharIndex", t->usFirstCharIndex, - "fsLastCharIndex", t->usLastCharIndex); + case FT_SFNT_OS2: { + auto t = static_cast(table); + return py::dict( + "version"_a=t->version, + "xAvgCharWidth"_a=t->xAvgCharWidth, + "usWeightClass"_a=t->usWeightClass, + "usWidthClass"_a=t->usWidthClass, + "fsType"_a=t->fsType, + "ySubscriptXSize"_a=t->ySubscriptXSize, + "ySubscriptYSize"_a=t->ySubscriptYSize, + "ySubscriptXOffset"_a=t->ySubscriptXOffset, + "ySubscriptYOffset"_a=t->ySubscriptYOffset, + "ySuperscriptXSize"_a=t->ySuperscriptXSize, + "ySuperscriptYSize"_a=t->ySuperscriptYSize, + "ySuperscriptXOffset"_a=t->ySuperscriptXOffset, + "ySuperscriptYOffset"_a=t->ySuperscriptYOffset, + "yStrikeoutSize"_a=t->yStrikeoutSize, + "yStrikeoutPosition"_a=t->yStrikeoutPosition, + "sFamilyClass"_a=t->sFamilyClass, + "panose"_a=py::bytes(reinterpret_cast(t->panose), 10), + "ulCharRange"_a=py::make_tuple(t->ulUnicodeRange1, t->ulUnicodeRange2, + t->ulUnicodeRange3, t->ulUnicodeRange4), + "achVendID"_a=py::bytes(reinterpret_cast(t->achVendID), 4), + "fsSelection"_a=t->fsSelection, + "fsFirstCharIndex"_a=t->usFirstCharIndex, + "fsLastCharIndex"_a=t->usLastCharIndex); } - case 3: { - char hhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_HoriHeader *t = (TT_HoriHeader *)table; - return Py_BuildValue(hhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "ascent", t->Ascender, - "descent", t->Descender, - "lineGap", t->Line_Gap, - "advanceWidthMax", t->advance_Width_Max, - "minLeftBearing", t->min_Left_Side_Bearing, - "minRightBearing", t->min_Right_Side_Bearing, - "xMaxExtent", t->xMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongHorMetrics", t->number_Of_HMetrics); + case FT_SFNT_HHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "ascent"_a=t->Ascender, + "descent"_a=t->Descender, + "lineGap"_a=t->Line_Gap, + "advanceWidthMax"_a=t->advance_Width_Max, + "minLeftBearing"_a=t->min_Left_Side_Bearing, + "minRightBearing"_a=t->min_Right_Side_Bearing, + "xMaxExtent"_a=t->xMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongHorMetrics"_a=t->number_Of_HMetrics); } - case 4: { - char vhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_VertHeader *t = (TT_VertHeader *)table; - return Py_BuildValue(vhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "vertTypoAscender", t->Ascender, - "vertTypoDescender", t->Descender, - "vertTypoLineGap", t->Line_Gap, - "advanceHeightMax", t->advance_Height_Max, - "minTopSideBearing", t->min_Top_Side_Bearing, - "minBottomSizeBearing", t->min_Bottom_Side_Bearing, - "yMaxExtent", t->yMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongVerMetrics", t->number_Of_VMetrics); + case FT_SFNT_VHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "vertTypoAscender"_a=t->Ascender, + "vertTypoDescender"_a=t->Descender, + "vertTypoLineGap"_a=t->Line_Gap, + "advanceHeightMax"_a=t->advance_Height_Max, + "minTopSideBearing"_a=t->min_Top_Side_Bearing, + "minBottomSizeBearing"_a=t->min_Bottom_Side_Bearing, + "yMaxExtent"_a=t->yMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongVerMetrics"_a=t->number_Of_VMetrics); } - case 5: { - char post_dict[] = "{s:(h,H), s:(h,H), s:h, s:h, s:k, s:k, s:k, s:k, s:k}"; - TT_Postscript *t = (TT_Postscript *)table; - return Py_BuildValue(post_dict, - "format", FIXED_MAJOR(t->FormatType), FIXED_MINOR(t->FormatType), - "italicAngle", FIXED_MAJOR(t->italicAngle), FIXED_MINOR(t->italicAngle), - "underlinePosition", t->underlinePosition, - "underlineThickness", t->underlineThickness, - "isFixedPitch", t->isFixedPitch, - "minMemType42", t->minMemType42, - "maxMemType42", t->maxMemType42, - "minMemType1", t->minMemType1, - "maxMemType1", t->maxMemType1); + case FT_SFNT_POST: { + auto t = static_cast(table); + return py::dict( + "format"_a=py::make_tuple(FIXED_MAJOR(t->FormatType), + FIXED_MINOR(t->FormatType)), + "italicAngle"_a=py::make_tuple(FIXED_MAJOR(t->italicAngle), + FIXED_MINOR(t->italicAngle)), + "underlinePosition"_a=t->underlinePosition, + "underlineThickness"_a=t->underlineThickness, + "isFixedPitch"_a=t->isFixedPitch, + "minMemType42"_a=t->minMemType42, + "maxMemType42"_a=t->maxMemType42, + "minMemType1"_a=t->minMemType1, + "maxMemType1"_a=t->maxMemType1); } - case 6: { - char pclt_dict[] = - "{s:(h,H), s:k, s:H, s:H, s:H, s:H, s:H, s:H, s:y#, s:y#, s:b, " - "s:b, s:b}"; - TT_PCLT *t = (TT_PCLT *)table; - return Py_BuildValue(pclt_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "fontNumber", t->FontNumber, - "pitch", t->Pitch, - "xHeight", t->xHeight, - "style", t->Style, - "typeFamily", t->TypeFamily, - "capHeight", t->CapHeight, - "symbolSet", t->SymbolSet, - "typeFace", t->TypeFace, Py_ssize_t(16), - "characterComplement", t->CharacterComplement, Py_ssize_t(8), - "strokeWeight", t->StrokeWeight, - "widthType", t->WidthType, - "serifStyle", t->SerifStyle); + case FT_SFNT_PCLT: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "fontNumber"_a=t->FontNumber, + "pitch"_a=t->Pitch, + "xHeight"_a=t->xHeight, + "style"_a=t->Style, + "typeFamily"_a=t->TypeFamily, + "capHeight"_a=t->CapHeight, + "symbolSet"_a=t->SymbolSet, + "typeFace"_a=py::bytes(reinterpret_cast(t->TypeFace), 16), + "characterComplement"_a=py::bytes( + reinterpret_cast(t->CharacterComplement), 8), + "strokeWeight"_a=t->StrokeWeight, + "widthType"_a=t->WidthType, + "serifStyle"_a=t->SerifStyle); } default: - Py_RETURN_NONE; + return std::nullopt; } } -const char *PyFT2Font_get_path__doc__ = - "get_path(self)\n" - "--\n\n" - "Get the path data from the currently loaded glyph as a tuple of vertices, " - "codes.\n"; +const char *PyFT2Font_get_path__doc__ = R"""( + Get the path data from the currently loaded glyph. + + Returns + ------- + vertices : np.ndarray[double] + The (N, 2) array of vertices describing the current glyph. + codes : np.ndarray[np.uint8] + The (N, ) array of codes corresponding to the vertices. + + See Also + -------- + .get_image + .load_char + .load_glyph + .set_text +)"""; -static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_path(PyFT2Font *self) { - CALL_CPP("get_path", return self->x->get_path()); + std::vector vertices; + std::vector codes; + + self->x->get_path(vertices, codes); + + py::ssize_t length = codes.size(); + py::ssize_t vertices_dims[2] = { length, 2 }; + py::array_t vertices_arr(vertices_dims); + if (length > 0) { + memcpy(vertices_arr.mutable_data(), vertices.data(), vertices_arr.nbytes()); + } + py::ssize_t codes_dims[1] = { length }; + py::array_t codes_arr(codes_dims); + if (length > 0) { + memcpy(codes_arr.mutable_data(), codes.data(), codes_arr.nbytes()); + } + + return py::make_tuple(vertices_arr, codes_arr); } -const char *PyFT2Font_get_image__doc__ = - "get_image(self)\n" - "--\n\n" - "Return the underlying image buffer for this font object.\n"; +const char *PyFT2Font_get_image__doc__ = R"""( + Return the underlying image buffer for this font object. + + Returns + ------- + np.ndarray[int] + + See Also + -------- + .get_path +)"""; -static PyObject *PyFT2Font_get_image(PyFT2Font *self, PyObject *args) +static py::array +PyFT2Font_get_image(PyFT2Font *self) { FT2Image &im = self->x->get_image(); - npy_intp dims[] = {(npy_intp)im.get_height(), (npy_intp)im.get_width() }; - return PyArray_SimpleNewFromData(2, dims, NPY_UBYTE, im.get_buffer()); + py::ssize_t dims[] = { + static_cast(im.get_height()), + static_cast(im.get_width()) + }; + return py::array_t(dims, im.get_buffer()); } -static PyObject *PyFT2Font_postscript_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_postscript_name(PyFT2Font *self) { const char *ps_name = FT_Get_Postscript_Name(self->x->get_face()); if (ps_name == NULL) { ps_name = "UNAVAILABLE"; } - return PyUnicode_FromString(ps_name); + return ps_name; } -static PyObject *PyFT2Font_num_faces(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_faces(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_faces); + return self->x->get_face()->num_faces; } -static PyObject *PyFT2Font_family_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_family_name(PyFT2Font *self) { const char *name = self->x->get_face()->family_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_style_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_style_name(PyFT2Font *self) { const char *name = self->x->get_face()->style_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_face_flags(PyFT2Font *self, void *closure) +static FaceFlags +PyFT2Font_face_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->face_flags); + return static_cast(self->x->get_face()->face_flags); } -static PyObject *PyFT2Font_style_flags(PyFT2Font *self, void *closure) +static StyleFlags +PyFT2Font_style_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->style_flags); + return static_cast(self->x->get_face()->style_flags); } -static PyObject *PyFT2Font_num_glyphs(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_glyphs(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_glyphs); + return self->x->get_face()->num_glyphs; } -static PyObject *PyFT2Font_num_fixed_sizes(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_fixed_sizes(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_fixed_sizes); + return self->x->get_face()->num_fixed_sizes; } -static PyObject *PyFT2Font_num_charmaps(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_charmaps(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_charmaps); + return self->x->get_face()->num_charmaps; } -static PyObject *PyFT2Font_scalable(PyFT2Font *self, void *closure) +static bool +PyFT2Font_scalable(PyFT2Font *self) { if (FT_IS_SCALABLE(self->x->get_face())) { - Py_RETURN_TRUE; + return true; } - Py_RETURN_FALSE; + return false; } -static PyObject *PyFT2Font_units_per_EM(PyFT2Font *self, void *closure) +static FT_UShort +PyFT2Font_units_per_EM(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->units_per_EM); + return self->x->get_face()->units_per_EM; } -static PyObject *PyFT2Font_get_bbox(PyFT2Font *self, void *closure) +static py::tuple +PyFT2Font_get_bbox(PyFT2Font *self) { FT_BBox *bbox = &(self->x->get_face()->bbox); - return Py_BuildValue("llll", - bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); + return py::make_tuple(bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); } -static PyObject *PyFT2Font_ascender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_ascender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->ascender); + return self->x->get_face()->ascender; } -static PyObject *PyFT2Font_descender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_descender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->descender); + return self->x->get_face()->descender; } -static PyObject *PyFT2Font_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->height); + return self->x->get_face()->height; } -static PyObject *PyFT2Font_max_advance_width(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_width(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_width); + return self->x->get_face()->max_advance_width; } -static PyObject *PyFT2Font_max_advance_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_height); + return self->x->get_face()->max_advance_height; } -static PyObject *PyFT2Font_underline_position(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_position(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_position); + return self->x->get_face()->underline_position; } -static PyObject *PyFT2Font_underline_thickness(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_thickness(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_thickness); + return self->x->get_face()->underline_thickness; } -static PyObject *PyFT2Font_fname(PyFT2Font *self, void *closure) +static py::str +PyFT2Font_fname(PyFT2Font *self) { if (self->stream.close) { // Called passed a filename to the constructor. - return PyObject_GetAttrString(self->py_file, "name"); + return self->py_file.attr("name"); } else { - Py_INCREF(self->py_file); - return self->py_file; + return py::cast(self->py_file); } } -static int PyFT2Font_get_buffer(PyFT2Font *self, Py_buffer *buf, int flags) -{ - FT2Image &im = self->x->get_image(); - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im.get_buffer(); - buf->len = im.get_width() * im.get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im.get_height(); - self->shape[1] = im.get_width(); - buf->shape = self->shape; - self->strides[0] = im.get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject *PyFT2Font_init_type() -{ - static PyGetSetDef getset[] = { - {(char *)"postscript_name", (getter)PyFT2Font_postscript_name, NULL, NULL, NULL}, - {(char *)"num_faces", (getter)PyFT2Font_num_faces, NULL, NULL, NULL}, - {(char *)"family_name", (getter)PyFT2Font_family_name, NULL, NULL, NULL}, - {(char *)"style_name", (getter)PyFT2Font_style_name, NULL, NULL, NULL}, - {(char *)"face_flags", (getter)PyFT2Font_face_flags, NULL, NULL, NULL}, - {(char *)"style_flags", (getter)PyFT2Font_style_flags, NULL, NULL, NULL}, - {(char *)"num_glyphs", (getter)PyFT2Font_num_glyphs, NULL, NULL, NULL}, - {(char *)"num_fixed_sizes", (getter)PyFT2Font_num_fixed_sizes, NULL, NULL, NULL}, - {(char *)"num_charmaps", (getter)PyFT2Font_num_charmaps, NULL, NULL, NULL}, - {(char *)"scalable", (getter)PyFT2Font_scalable, NULL, NULL, NULL}, - {(char *)"units_per_EM", (getter)PyFT2Font_units_per_EM, NULL, NULL, NULL}, - {(char *)"bbox", (getter)PyFT2Font_get_bbox, NULL, NULL, NULL}, - {(char *)"ascender", (getter)PyFT2Font_ascender, NULL, NULL, NULL}, - {(char *)"descender", (getter)PyFT2Font_descender, NULL, NULL, NULL}, - {(char *)"height", (getter)PyFT2Font_height, NULL, NULL, NULL}, - {(char *)"max_advance_width", (getter)PyFT2Font_max_advance_width, NULL, NULL, NULL}, - {(char *)"max_advance_height", (getter)PyFT2Font_max_advance_height, NULL, NULL, NULL}, - {(char *)"underline_position", (getter)PyFT2Font_underline_position, NULL, NULL, NULL}, - {(char *)"underline_thickness", (getter)PyFT2Font_underline_thickness, NULL, NULL, NULL}, - {(char *)"fname", (getter)PyFT2Font_fname, NULL, NULL, NULL}, - {NULL} - }; - - static PyMethodDef methods[] = { - {"clear", (PyCFunction)PyFT2Font_clear, METH_NOARGS, PyFT2Font_clear__doc__}, - {"set_size", (PyCFunction)PyFT2Font_set_size, METH_VARARGS, PyFT2Font_set_size__doc__}, - {"set_charmap", (PyCFunction)PyFT2Font_set_charmap, METH_VARARGS, PyFT2Font_set_charmap__doc__}, - {"select_charmap", (PyCFunction)PyFT2Font_select_charmap, METH_VARARGS, PyFT2Font_select_charmap__doc__}, - {"get_kerning", (PyCFunction)PyFT2Font_get_kerning, METH_VARARGS, PyFT2Font_get_kerning__doc__}, - {"set_text", (PyCFunction)PyFT2Font_set_text, METH_VARARGS|METH_KEYWORDS, PyFT2Font_set_text__doc__}, - {"_get_fontmap", (PyCFunction)PyFT2Font_get_fontmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_fontmap__doc__}, - {"get_num_glyphs", (PyCFunction)PyFT2Font_get_num_glyphs, METH_NOARGS, PyFT2Font_get_num_glyphs__doc__}, - {"load_char", (PyCFunction)PyFT2Font_load_char, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_char__doc__}, - {"load_glyph", (PyCFunction)PyFT2Font_load_glyph, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_glyph__doc__}, - {"get_width_height", (PyCFunction)PyFT2Font_get_width_height, METH_NOARGS, PyFT2Font_get_width_height__doc__}, - {"get_bitmap_offset", (PyCFunction)PyFT2Font_get_bitmap_offset, METH_NOARGS, PyFT2Font_get_bitmap_offset__doc__}, - {"get_descent", (PyCFunction)PyFT2Font_get_descent, METH_NOARGS, PyFT2Font_get_descent__doc__}, - {"draw_glyphs_to_bitmap", (PyCFunction)PyFT2Font_draw_glyphs_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyphs_to_bitmap__doc__}, - {"get_xys", (PyCFunction)PyFT2Font_get_xys, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_xys__doc__}, - {"draw_glyph_to_bitmap", (PyCFunction)PyFT2Font_draw_glyph_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyph_to_bitmap__doc__}, - {"get_glyph_name", (PyCFunction)PyFT2Font_get_glyph_name, METH_VARARGS, PyFT2Font_get_glyph_name__doc__}, - {"get_charmap", (PyCFunction)PyFT2Font_get_charmap, METH_NOARGS, PyFT2Font_get_charmap__doc__}, - {"get_char_index", (PyCFunction)PyFT2Font_get_char_index, METH_VARARGS, PyFT2Font_get_char_index__doc__}, - {"get_sfnt", (PyCFunction)PyFT2Font_get_sfnt, METH_NOARGS, PyFT2Font_get_sfnt__doc__}, - {"get_name_index", (PyCFunction)PyFT2Font_get_name_index, METH_VARARGS, PyFT2Font_get_name_index__doc__}, - {"get_ps_font_info", (PyCFunction)PyFT2Font_get_ps_font_info, METH_NOARGS, PyFT2Font_get_ps_font_info__doc__}, - {"get_sfnt_table", (PyCFunction)PyFT2Font_get_sfnt_table, METH_VARARGS, PyFT2Font_get_sfnt_table__doc__}, - {"get_path", (PyCFunction)PyFT2Font_get_path, METH_NOARGS, PyFT2Font_get_path__doc__}, - {"get_image", (PyCFunction)PyFT2Font_get_image, METH_NOARGS, PyFT2Font_get_image__doc__}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Font_get_buffer; - - PyFT2FontType.tp_name = "matplotlib.ft2font.FT2Font"; - PyFT2FontType.tp_doc = PyFT2Font_init__doc__; - PyFT2FontType.tp_basicsize = sizeof(PyFT2Font); - PyFT2FontType.tp_dealloc = (destructor)PyFT2Font_dealloc; - PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyFT2FontType.tp_methods = methods; - PyFT2FontType.tp_getset = getset; - PyFT2FontType.tp_new = PyFT2Font_new; - PyFT2FontType.tp_init = (initproc)PyFT2Font_init; - PyFT2FontType.tp_as_buffer = &buffer_procs; - - return &PyFT2FontType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "ft2font" }; - -PyMODINIT_FUNC PyInit_ft2font(void) +static py::object +ft2font__getattr__(std::string name) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + +#define DEPRECATE_ATTR_FROM_ENUM(attr_, alternative_, real_value_) \ + do { \ + if (name == #attr_) { \ + warn("since"_a="3.10", "name"_a=#attr_, "obj_type"_a="attribute", \ + "alternative"_a=#alternative_); \ + return py::cast(static_cast(real_value_)); \ + } \ + } while(0) + DEPRECATE_ATTR_FROM_ENUM(KERNING_DEFAULT, Kerning.DEFAULT, FT_KERNING_DEFAULT); + DEPRECATE_ATTR_FROM_ENUM(KERNING_UNFITTED, Kerning.UNFITTED, FT_KERNING_UNFITTED); + DEPRECATE_ATTR_FROM_ENUM(KERNING_UNSCALED, Kerning.UNSCALED, FT_KERNING_UNSCALED); + +#undef DEPRECATE_ATTR_FROM_ENUM + +#define DEPRECATE_ATTR_FROM_FLAG(attr_, enum_, value_) \ + do { \ + if (name == #attr_) { \ + warn("since"_a="3.10", "name"_a=#attr_, "obj_type"_a="attribute", \ + "alternative"_a=#enum_ "." #value_); \ + return py::cast(enum_::value_); \ + } \ + } while(0) + + DEPRECATE_ATTR_FROM_FLAG(LOAD_DEFAULT, LoadFlags, DEFAULT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_SCALE, LoadFlags, NO_SCALE); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_HINTING, LoadFlags, NO_HINTING); + DEPRECATE_ATTR_FROM_FLAG(LOAD_RENDER, LoadFlags, RENDER); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_BITMAP, LoadFlags, NO_BITMAP); + DEPRECATE_ATTR_FROM_FLAG(LOAD_VERTICAL_LAYOUT, LoadFlags, VERTICAL_LAYOUT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_FORCE_AUTOHINT, LoadFlags, FORCE_AUTOHINT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_CROP_BITMAP, LoadFlags, CROP_BITMAP); + DEPRECATE_ATTR_FROM_FLAG(LOAD_PEDANTIC, LoadFlags, PEDANTIC); + DEPRECATE_ATTR_FROM_FLAG(LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, LoadFlags, + IGNORE_GLOBAL_ADVANCE_WIDTH); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_RECURSE, LoadFlags, NO_RECURSE); + DEPRECATE_ATTR_FROM_FLAG(LOAD_IGNORE_TRANSFORM, LoadFlags, IGNORE_TRANSFORM); + DEPRECATE_ATTR_FROM_FLAG(LOAD_MONOCHROME, LoadFlags, MONOCHROME); + DEPRECATE_ATTR_FROM_FLAG(LOAD_LINEAR_DESIGN, LoadFlags, LINEAR_DESIGN); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_AUTOHINT, LoadFlags, NO_AUTOHINT); + + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_NORMAL, LoadFlags, TARGET_NORMAL); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LIGHT, LoadFlags, TARGET_LIGHT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_MONO, LoadFlags, TARGET_MONO); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LCD, LoadFlags, TARGET_LCD); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LCD_V, LoadFlags, TARGET_LCD_V); + + DEPRECATE_ATTR_FROM_FLAG(SCALABLE, FaceFlags, SCALABLE); + DEPRECATE_ATTR_FROM_FLAG(FIXED_SIZES, FaceFlags, FIXED_SIZES); + DEPRECATE_ATTR_FROM_FLAG(FIXED_WIDTH, FaceFlags, FIXED_WIDTH); + DEPRECATE_ATTR_FROM_FLAG(SFNT, FaceFlags, SFNT); + DEPRECATE_ATTR_FROM_FLAG(HORIZONTAL, FaceFlags, HORIZONTAL); + DEPRECATE_ATTR_FROM_FLAG(VERTICAL, FaceFlags, VERTICAL); + DEPRECATE_ATTR_FROM_FLAG(KERNING, FaceFlags, KERNING); + DEPRECATE_ATTR_FROM_FLAG(FAST_GLYPHS, FaceFlags, FAST_GLYPHS); + DEPRECATE_ATTR_FROM_FLAG(MULTIPLE_MASTERS, FaceFlags, MULTIPLE_MASTERS); + DEPRECATE_ATTR_FROM_FLAG(GLYPH_NAMES, FaceFlags, GLYPH_NAMES); + DEPRECATE_ATTR_FROM_FLAG(EXTERNAL_STREAM, FaceFlags, EXTERNAL_STREAM); + + DEPRECATE_ATTR_FROM_FLAG(ITALIC, StyleFlags, ITALIC); + DEPRECATE_ATTR_FROM_FLAG(BOLD, StyleFlags, BOLD); +#undef DEPRECATE_ATTR_FROM_FLAG + + throw py::attribute_error( + "module 'matplotlib.ft2font' has no attribute {!r}"_s.format(name)); +} + +PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) { - import_array(); - if (FT_Init_FreeType(&_ft2Library)) { // initialize library - return PyErr_Format( - PyExc_RuntimeError, "Could not initialize the freetype2 library"); + throw std::runtime_error("Could not initialize the freetype2 library"); } FT_Int major, minor, patch; char version_string[64]; FT_Library_Version(_ft2Library, &major, &minor, &patch); snprintf(version_string, sizeof(version_string), "%d.%d.%d", major, minor, patch); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) || - prepare_and_add_type(PyFT2Image_init_type(), m) || - prepare_and_add_type(PyFT2Font_init_type(), m) || - // Glyph is not constructible from Python, thus not added to the module. - PyType_Ready(PyGlyph_init_type()) || - PyModule_AddStringConstant(m, "__freetype_version__", version_string) || - PyModule_AddStringConstant(m, "__freetype_build_type__", FREETYPE_BUILD_TYPE) || - PyModule_AddIntConstant(m, "SCALABLE", FT_FACE_FLAG_SCALABLE) || - PyModule_AddIntConstant(m, "FIXED_SIZES", FT_FACE_FLAG_FIXED_SIZES) || - PyModule_AddIntConstant(m, "FIXED_WIDTH", FT_FACE_FLAG_FIXED_WIDTH) || - PyModule_AddIntConstant(m, "SFNT", FT_FACE_FLAG_SFNT) || - PyModule_AddIntConstant(m, "HORIZONTAL", FT_FACE_FLAG_HORIZONTAL) || - PyModule_AddIntConstant(m, "VERTICAL", FT_FACE_FLAG_VERTICAL) || - PyModule_AddIntConstant(m, "KERNING", FT_FACE_FLAG_KERNING) || - PyModule_AddIntConstant(m, "FAST_GLYPHS", FT_FACE_FLAG_FAST_GLYPHS) || - PyModule_AddIntConstant(m, "MULTIPLE_MASTERS", FT_FACE_FLAG_MULTIPLE_MASTERS) || - PyModule_AddIntConstant(m, "GLYPH_NAMES", FT_FACE_FLAG_GLYPH_NAMES) || - PyModule_AddIntConstant(m, "EXTERNAL_STREAM", FT_FACE_FLAG_EXTERNAL_STREAM) || - PyModule_AddIntConstant(m, "ITALIC", FT_STYLE_FLAG_ITALIC) || - PyModule_AddIntConstant(m, "BOLD", FT_STYLE_FLAG_BOLD) || - PyModule_AddIntConstant(m, "KERNING_DEFAULT", FT_KERNING_DEFAULT) || - PyModule_AddIntConstant(m, "KERNING_UNFITTED", FT_KERNING_UNFITTED) || - PyModule_AddIntConstant(m, "KERNING_UNSCALED", FT_KERNING_UNSCALED) || - PyModule_AddIntConstant(m, "LOAD_DEFAULT", FT_LOAD_DEFAULT) || - PyModule_AddIntConstant(m, "LOAD_NO_SCALE", FT_LOAD_NO_SCALE) || - PyModule_AddIntConstant(m, "LOAD_NO_HINTING", FT_LOAD_NO_HINTING) || - PyModule_AddIntConstant(m, "LOAD_RENDER", FT_LOAD_RENDER) || - PyModule_AddIntConstant(m, "LOAD_NO_BITMAP", FT_LOAD_NO_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_VERTICAL_LAYOUT", FT_LOAD_VERTICAL_LAYOUT) || - PyModule_AddIntConstant(m, "LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_CROP_BITMAP", FT_LOAD_CROP_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_PEDANTIC", FT_LOAD_PEDANTIC) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH", FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH) || - PyModule_AddIntConstant(m, "LOAD_NO_RECURSE", FT_LOAD_NO_RECURSE) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_TRANSFORM", FT_LOAD_IGNORE_TRANSFORM) || - PyModule_AddIntConstant(m, "LOAD_MONOCHROME", FT_LOAD_MONOCHROME) || - PyModule_AddIntConstant(m, "LOAD_LINEAR_DESIGN", FT_LOAD_LINEAR_DESIGN) || - PyModule_AddIntConstant(m, "LOAD_NO_AUTOHINT", (unsigned long)FT_LOAD_NO_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_NORMAL", (unsigned long)FT_LOAD_TARGET_NORMAL) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LIGHT", (unsigned long)FT_LOAD_TARGET_LIGHT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_MONO", (unsigned long)FT_LOAD_TARGET_MONO) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD", (unsigned long)FT_LOAD_TARGET_LCD) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD_V", (unsigned long)FT_LOAD_TARGET_LCD_V)) { - FT_Done_FreeType(_ft2Library); - Py_XDECREF(m); - return NULL; - } - - return m; + p11x::bind_enums(m); + p11x::enums["Kerning"].attr("__doc__") = Kerning__doc__; + p11x::enums["LoadFlags"].attr("__doc__") = LoadFlags__doc__; + p11x::enums["FaceFlags"].attr("__doc__") = FaceFlags__doc__; + p11x::enums["StyleFlags"].attr("__doc__") = StyleFlags__doc__; + + py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol(), + PyFT2Image__doc__) + .def(py::init( + [](double_or_ width, double_or_ height) { + return new FT2Image( + _double_to_("width", width), + _double_to_("height", height) + ); + }), + "width"_a, "height"_a, PyFT2Image_init__doc__) + .def("draw_rect_filled", &PyFT2Image_draw_rect_filled, + "x0"_a, "y0"_a, "x1"_a, "y1"_a, + PyFT2Image_draw_rect_filled__doc__) + .def_buffer([](FT2Image &self) -> py::buffer_info { + std::vector shape { self.get_height(), self.get_width() }; + std::vector strides { self.get_width(), 1 }; + return py::buffer_info(self.get_buffer(), shape, strides); + }); + + py::class_(m, "Glyph", py::is_final(), PyGlyph__doc__) + .def(py::init<>([]() -> PyGlyph { + // Glyph is not useful from Python, so mark it as not constructible. + throw std::runtime_error("Glyph is not constructible"); + })) + .def_readonly("width", &PyGlyph::width, "The glyph's width.") + .def_readonly("height", &PyGlyph::height, "The glyph's height.") + .def_readonly("horiBearingX", &PyGlyph::horiBearingX, + "Left side bearing for horizontal layout.") + .def_readonly("horiBearingY", &PyGlyph::horiBearingY, + "Top side bearing for horizontal layout.") + .def_readonly("horiAdvance", &PyGlyph::horiAdvance, + "Advance width for horizontal layout.") + .def_readonly("linearHoriAdvance", &PyGlyph::linearHoriAdvance, + "The advance width of the unhinted glyph.") + .def_readonly("vertBearingX", &PyGlyph::vertBearingX, + "Left side bearing for vertical layout.") + .def_readonly("vertBearingY", &PyGlyph::vertBearingY, + "Top side bearing for vertical layout.") + .def_readonly("vertAdvance", &PyGlyph::vertAdvance, + "Advance height for vertical layout.") + .def_property_readonly("bbox", &PyGlyph_get_bbox, + "The control box of the glyph."); + + py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), + PyFT2Font__doc__) + .def(py::init(&PyFT2Font_init), + "filename"_a, "hinting_factor"_a=8, py::kw_only(), + "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, + PyFT2Font_init__doc__) + .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) + .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a, + PyFT2Font_set_size__doc__) + .def("set_charmap", &PyFT2Font_set_charmap, "i"_a, + PyFT2Font_set_charmap__doc__) + .def("select_charmap", &PyFT2Font_select_charmap, "i"_a, + PyFT2Font_select_charmap__doc__) + .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, + PyFT2Font_get_kerning__doc__) + .def("set_text", &PyFT2Font_set_text, + "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, + PyFT2Font_set_text__doc__) + .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, + PyFT2Font_get_fontmap__doc__) + .def("get_num_glyphs", &PyFT2Font_get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) + .def("load_char", &PyFT2Font_load_char, + "charcode"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, + PyFT2Font_load_char__doc__) + .def("load_glyph", &PyFT2Font_load_glyph, + "glyph_index"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, + PyFT2Font_load_glyph__doc__) + .def("get_width_height", &PyFT2Font_get_width_height, + PyFT2Font_get_width_height__doc__) + .def("get_bitmap_offset", &PyFT2Font_get_bitmap_offset, + PyFT2Font_get_bitmap_offset__doc__) + .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) + .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, + py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyphs_to_bitmap__doc__) + .def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap, + "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyph_to_bitmap__doc__) + .def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a, + PyFT2Font_get_glyph_name__doc__) + .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__) + .def("get_char_index", &PyFT2Font_get_char_index, "codepoint"_a, + PyFT2Font_get_char_index__doc__) + .def("get_sfnt", &PyFT2Font_get_sfnt, PyFT2Font_get_sfnt__doc__) + .def("get_name_index", &PyFT2Font_get_name_index, "name"_a, + PyFT2Font_get_name_index__doc__) + .def("get_ps_font_info", &PyFT2Font_get_ps_font_info, + PyFT2Font_get_ps_font_info__doc__) + .def("get_sfnt_table", &PyFT2Font_get_sfnt_table, "name"_a, + PyFT2Font_get_sfnt_table__doc__) + .def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__) + .def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__) + + .def_property_readonly("postscript_name", &PyFT2Font_postscript_name, + "PostScript name of the font.") + .def_property_readonly("num_faces", &PyFT2Font_num_faces, + "Number of faces in file.") + .def_property_readonly("family_name", &PyFT2Font_family_name, + "Face family name.") + .def_property_readonly("style_name", &PyFT2Font_style_name, + "Style name.") + .def_property_readonly("face_flags", &PyFT2Font_face_flags, + "Face flags; see `.FaceFlags`.") + .def_property_readonly("style_flags", &PyFT2Font_style_flags, + "Style flags; see `.StyleFlags`.") + .def_property_readonly("num_glyphs", &PyFT2Font_num_glyphs, + "Number of glyphs in the face.") + .def_property_readonly("num_fixed_sizes", &PyFT2Font_num_fixed_sizes, + "Number of bitmap in the face.") + .def_property_readonly("num_charmaps", &PyFT2Font_num_charmaps, + "Number of charmaps in the face.") + .def_property_readonly("scalable", &PyFT2Font_scalable, + "Whether face is scalable; attributes after this one " + "are only defined for scalable faces.") + .def_property_readonly("units_per_EM", &PyFT2Font_units_per_EM, + "Number of font units covered by the EM.") + .def_property_readonly("bbox", &PyFT2Font_get_bbox, + "Face global bounding box (xmin, ymin, xmax, ymax).") + .def_property_readonly("ascender", &PyFT2Font_ascender, + "Ascender in 26.6 units.") + .def_property_readonly("descender", &PyFT2Font_descender, + "Descender in 26.6 units.") + .def_property_readonly("height", &PyFT2Font_height, + "Height in 26.6 units; used to compute a default line " + "spacing (baseline-to-baseline distance).") + .def_property_readonly("max_advance_width", &PyFT2Font_max_advance_width, + "Maximum horizontal cursor advance for all glyphs.") + .def_property_readonly("max_advance_height", &PyFT2Font_max_advance_height, + "Maximum vertical cursor advance for all glyphs.") + .def_property_readonly("underline_position", &PyFT2Font_underline_position, + "Vertical position of the underline bar.") + .def_property_readonly("underline_thickness", &PyFT2Font_underline_thickness, + "Thickness of the underline bar.") + .def_property_readonly("fname", &PyFT2Font_fname, + "The original filename for this object.") + + .def_buffer([](PyFT2Font &self) -> py::buffer_info { + FT2Image &im = self.x->get_image(); + std::vector shape { im.get_height(), im.get_width() }; + std::vector strides { im.get_width(), 1 }; + return py::buffer_info(im.get_buffer(), shape, strides); + }); + + m.attr("__freetype_version__") = version_string; + m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + m.def("__getattr__", ft2font__getattr__); } diff --git a/src/meson.build b/src/meson.build index bbef93c13d92..a7018f0db094 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,42 +1,3 @@ -# NumPy include directory - needed in all submodules -# The try-except is needed because when things are split across drives on Windows, there -# is no relative path and an exception gets raised. There may be other such cases, so add -# a catch-all and switch to an absolute path. Relative paths are needed when for example -# a virtualenv is placed inside the source tree; Meson rejects absolute paths to places -# inside the source tree. -# For cross-compilation it is often not possible to run the Python interpreter in order -# to retrieve numpy's include directory. It can be specified in the cross file instead: -# -# [properties] -# numpy-include-dir = /abspath/to/host-pythons/site-packages/numpy/core/include -# -# This uses the path as is, and avoids running the interpreter. -incdir_numpy = meson.get_external_property('numpy-include-dir', 'not-given') -if incdir_numpy == 'not-given' - incdir_numpy = run_command(py3, - [ - '-c', - '''import os -import numpy as np -try: - incdir = os.path.relpath(np.get_include()) -except Exception: - incdir = np.get_include() -print(incdir)''' - ], - check: true - ).stdout().strip() -endif -numpy_dep = declare_dependency( - compile_args: [ - '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION', - # Allow NumPy's printf format specifiers in C++. - '-D__STDC_FORMAT_MACROS=1', - ], - include_directories: include_directories(incdir_numpy), - dependencies: py3_dep, -) - # For cross-compilation it is often not possible to run the Python interpreter in order # to retrieve the platform-specific /dev/null. It can be specified in the cross file # instead: @@ -73,11 +34,10 @@ extension_data = { '_backend_agg': { 'subdir': 'matplotlib/backends', 'sources': files( - 'py_converters.cpp', '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), - 'dependencies': [agg_dep, numpy_dep, freetype_dep], + 'dependencies': [agg_dep, freetype_dep, pybind11_dep], }, '_c_internal_utils': { 'subdir': 'matplotlib', @@ -91,10 +51,9 @@ extension_data = { 'sources': files( 'ft2font.cpp', 'ft2font_wrapper.cpp', - 'py_converters.cpp', ), 'dependencies': [ - freetype_dep, numpy_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( @@ -106,7 +65,7 @@ extension_data = { 'subdir': 'matplotlib', 'sources': files( '_image_wrapper.cpp', - 'py_converters_11.cpp', + 'py_converters.cpp', ), 'dependencies': [ pybind11_dep, @@ -117,11 +76,9 @@ extension_data = { '_path': { 'subdir': 'matplotlib', 'sources': files( - 'py_converters.cpp', - 'py_converters_11.cpp', '_path_wrapper.cpp', ), - 'dependencies': [numpy_dep, agg_dep, pybind11_dep], + 'dependencies': [agg_dep, pybind11_dep], }, '_qhull': { 'subdir': 'matplotlib', @@ -151,30 +108,26 @@ extension_data = { ), 'dependencies': [pybind11_dep], }, - '_ttconv': { - 'subdir': 'matplotlib', - 'sources': files( - '_ttconv.cpp', - ), - 'dependencies': [ttconv_dep, pybind11_dep], - }, } -cpp_special_arguments = [] -if cpp.get_id() == 'msvc' and get_option('buildtype') != 'plain' - # Disable FH4 Exception Handling implementation so that we don't require - # VCRUNTIME140_1.dll. For more details, see: - # https://devblogs.microsoft.com/cppblog/making-cpp-exception-handling-smaller-x64/ - # https://github.com/joerick/cibuildwheel/issues/423#issuecomment-677763904 - cpp_special_arguments += ['/d2FH4-'] +if cpp.get_id() == 'msvc' + # This flag fixes some bugs with the macro processing, namely + # https://learn.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=msvc-170#macro-arguments-are-unpacked + if cpp.has_argument('/Zc:preprocessor') + # This flag was added in MSVC 2019 version 16.5, which deprecated the one below. + new_preprocessor = '/Zc:preprocessor' + else + # Since we currently support any version of MSVC 2019 (vc142), we'll stick with the + # older flag, added in MSVC 2017 version 15.8. + new_preprocessor = '/experimental:preprocessor' + endif +else + new_preprocessor = [] endif foreach ext, kwargs : extension_data - # Ensure that PY_ARRAY_UNIQUE_SYMBOL is uniquely defined for each extension. - unique_array_api = '-DPY_ARRAY_UNIQUE_SYMBOL=MPL_@0@_ARRAY_API'.format(ext.replace('.', '_')) additions = { - 'c_args': [unique_array_api] + kwargs.get('c_args', []), - 'cpp_args': cpp_special_arguments + [unique_array_api] + kwargs.get('cpp_args', []), + 'cpp_args': [new_preprocessor] + kwargs.get('cpp_args', []), } py3.extension_module( ext, diff --git a/src/mplutils.h b/src/mplutils.h index 58eb32d190ec..95d9a2d9eb90 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -48,48 +48,70 @@ enum { CLOSEPOLY = 0x4f }; -inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) -{ - if (PyType_Ready(type)) { - return -1; - } - char const* ptr = strrchr(type->tp_name, '.'); - if (!ptr) { - PyErr_SetString(PyExc_ValueError, "tp_name should be a qualified name"); - return -1; - } - if (PyModule_AddObject(module, ptr + 1, (PyObject *)type)) { - return -1; - } - return 0; -} - #ifdef __cplusplus // not for macosx.m // Check that array has shape (N, d1) or (N, d1, d2). We cast d1, d2 to longs // so that we don't need to access the NPY_INTP_FMT macro here. +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; template -inline bool check_trailing_shape(T array, char const* name, long d1) +inline void check_trailing_shape(T array, char const* name, long d1) { + if (array.ndim() != 2) { + throw py::value_error( + "Expected 2-dimensional array, got %d"_s.format(array.ndim())); + } + if (array.size() == 0) { + // Sometimes things come through as atleast_2d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return; + } if (array.shape(1) != d1) { - PyErr_Format(PyExc_ValueError, - "%s must have shape (N, %ld), got (%ld, %ld)", - name, d1, array.shape(0), array.shape(1)); - return false; + throw py::value_error( + "%s must have shape (N, %d), got (%d, %d)"_s.format( + name, d1, array.shape(0), array.shape(1))); } - return true; } template -inline bool check_trailing_shape(T array, char const* name, long d1, long d2) +inline void check_trailing_shape(T array, char const* name, long d1, long d2) { + if (array.ndim() != 3) { + throw py::value_error( + "Expected 3-dimensional array, got %d"_s.format(array.ndim())); + } + if (array.size() == 0) { + // Sometimes things come through as atleast_3d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return; + } if (array.shape(1) != d1 || array.shape(2) != d2) { - PyErr_Format(PyExc_ValueError, - "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", - name, d1, d2, array.shape(0), array.shape(1), array.shape(2)); - return false; + throw py::value_error( + "%s must have shape (N, %d, %d), got (%d, %d, %d)"_s.format( + name, d1, d2, array.shape(0), array.shape(1), array.shape(2))); + } +} + +/* In most cases, code should use safe_first_shape(obj) instead of obj.shape(0), since + safe_first_shape(obj) == 0 when any dimension is 0. */ +template +py::ssize_t +safe_first_shape(const py::detail::unchecked_reference &a) +{ + bool empty = (ND == 0); + for (py::ssize_t i = 0; i < ND; i++) { + if (a.shape(i) == 0) { + empty = true; + } + } + if (empty) { + return 0; + } else { + return a.shape(0); } - return true; } #endif diff --git a/src/numpy_cpp.h b/src/numpy_cpp.h deleted file mode 100644 index 6165789b7603..000000000000 --- a/src/numpy_cpp.h +++ /dev/null @@ -1,582 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#ifndef MPL_NUMPY_CPP_H -#define MPL_NUMPY_CPP_H -#define PY_SSIZE_T_CLEAN -/*************************************************************************** - * This file is based on original work by Mark Wiebe, available at: - * - * http://github.com/mwiebe/numpy-cpp - * - * However, the needs of matplotlib wrappers, such as treating an - * empty array as having the correct dimensions, have made this rather - * matplotlib-specific, so it's no longer compatible with the - * original. - */ - -#ifdef _POSIX_C_SOURCE -# undef _POSIX_C_SOURCE -#endif -#ifndef _AIX -#ifdef _XOPEN_SOURCE -# undef _XOPEN_SOURCE -#endif -#endif - -// Prevent multiple conflicting definitions of swab from stdlib.h and unistd.h -#if defined(__sun) || defined(sun) -#if defined(_XPG4) -#undef _XPG4 -#endif -#if defined(_XPG3) -#undef _XPG3 -#endif -#endif - -#include -#include - -#include "py_exceptions.h" - -#include - -namespace numpy -{ - -// Type traits for the NumPy types -template -struct type_num_of; - -/* Be careful with bool arrays as python has sizeof(npy_bool) == 1, but it is - * not always the case that sizeof(bool) == 1. Using the array_view_accessors - * is always fine regardless of sizeof(bool), so do this rather than using - * array.data() and pointer arithmetic which will not work correctly if - * sizeof(bool) != 1. */ -template <> struct type_num_of -{ - enum { - value = NPY_BOOL - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_BYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UBYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_SHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_USHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_INT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UINT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_FLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_DOUBLE - }; -}; -#if NPY_LONGDOUBLE != NPY_DOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_LONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_CDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CDOUBLE - }; -}; -#if NPY_CLONGDOUBLE != NPY_CDOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_OBJECT - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; - -template -struct is_const -{ - enum { - value = false - }; -}; -template -struct is_const -{ - enum { - value = true - }; -}; - -namespace detail -{ -template