diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 8ec11e30e122..9d4de069b078 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -46,7 +46,7 @@ jobs: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 name: Install Python with: python-version: '3.11' @@ -143,7 +143,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.14 - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -153,7 +153,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -162,7 +162,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -170,7 +170,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -178,7 +178,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -215,9 +215,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-path: dist/matplotlib-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index 9a2516efd4bb..3838a38004e0 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -41,7 +41,7 @@ jobs: - name: Set up reviewdog if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 + uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.2 with: reviewdog_version: latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 07c9c1701cf1..eebdd65105e3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/init@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10 with: languages: ${{ matrix.language }} @@ -43,4 +43,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/analyze@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 17c4922df054..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@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 with: sync-labels: true diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f1c6d21019e3..f5cada1f3f9d 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.x" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.11' @@ -40,7 +40,7 @@ jobs: run: pip3 install ruff - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 + uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 - name: Run ruff env: @@ -61,7 +61,7 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.11' @@ -69,7 +69,7 @@ jobs: run: pip3 install -r requirements/testing/mypy.txt -r requirements/testing/all.txt - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 + uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 - name: Run mypy env: @@ -92,7 +92,7 @@ jobs: persist-credentials: false - name: eslint - uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0 + uses: reviewdog/action-eslint@2fee6dd72a5419ff4113f694e2068d2a03bb35dd # v1.33.2 with: filter_mode: nofilter github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 3815efd08954..b40909b371a6 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -17,12 +17,12 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.11' - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 + uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 - name: Install tox run: python -m pip install tox diff --git a/.github/workflows/pr_welcome.yml b/.github/workflows/pr_welcome.yml index 874f8807b478..0a654753861a 100644 --- a/.github/workflows/pr_welcome.yml +++ b/.github/workflows/pr_welcome.yml @@ -11,8 +11,8 @@ jobs: steps: - uses: actions/first-interaction@753c925c8d1ac6fede23781875376600628d9b5d # v3.0.0 with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - pr_message: >+ + repo-token: ${{ secrets.GITHUB_TOKEN }} + pr-message: >+ Thank you for opening your first PR into Matplotlib! diff --git a/.github/workflows/stale-tidy.yml b/.github/workflows/stale-tidy.yml index 09b9cc49a8f8..bc50dc892155 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@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 300 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 624792ed171a..b65b44a59e88 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@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 20 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c6f1e25c1e92..e965819628be 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,7 +96,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -396,7 +396,7 @@ jobs: fi - name: Upload code coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}" token: ${{ secrets.CODECOV_TOKEN }} diff --git a/doc/api/next_api_changes/behavior/30532-TH.rst b/doc/api/next_api_changes/behavior/30532-TH.rst deleted file mode 100644 index 3d368c566039..000000000000 --- a/doc/api/next_api_changes/behavior/30532-TH.rst +++ /dev/null @@ -1,4 +0,0 @@ -Default name of ``ListedColormap`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The default name of `.ListedColormap` has changed from "from_list" to "unnamed". diff --git a/doc/api/next_api_changes/deprecations/30364-AS.rst b/doc/api/next_api_changes/deprecations/30364-AS.rst deleted file mode 100644 index 4f5493b8b706..000000000000 --- a/doc/api/next_api_changes/deprecations/30364-AS.rst +++ /dev/null @@ -1,4 +0,0 @@ -Parameters ``Axes3D.set_aspect(..., anchor=..., share=...)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The parameters *anchor* and *share* of `.Axes3D.set_aspect` are deprecated. -They had no effect on 3D axes and will be removed in a future version. diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index e2291e3255e6..558e19790d82 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -29,8 +29,7 @@ Ways to contribute * **You are a Matplotlib user, and you see a bug, a potential improvement, or something that annoys you, and you can fix it.** - You can search our `issue tracker `__ - for an existing issue that describes your problem or + You can search our issue tracker for an existing issue that describes your problem or open a new issue to inform us of the problem you observed and discuss the best approach to fix it. If your contributions would not be captured on GitHub (social media, communication, educational content), you can also reach out to us on gitter_, @@ -43,11 +42,14 @@ Ways to contribute Awesome — you have a focus on a specific application and domain and can start there. In this case, maintainers can help you figure out the best - implementation; `open an issue `__ - in our issue tracker, and we'll be happy to discuss technical approaches. + implementation; open an issue or pull request with a starting point, and we'll + be happy to discuss technical approaches. - If you can implement the solution yourself, even better! Consider contributing - the change as a :ref:`pull request ` right away. + If you prefer, you can use the `GitHub functionality for "draft" pull requests + `__ + and request early feedback on whatever you are working on, but you should be + aware that maintainers may not review your contribution unless it has the + "Ready to review" state on GitHub. * **You are new to Matplotlib, both as a user and contributor, and want to start contributing but have yet to develop a particular interest.** @@ -285,7 +287,7 @@ guide you through each step: 4. Check existing pull requests (e.g., :ghpull:`28476`) and filter by the issue number to make sure the issue is not in progress: * If the issue has a pull request (is in progress), tag the user working on the issue, and ask to collaborate (optional). - * If there is no pull request, :ref:`create a new pull request `. + * 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 `_. diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index c0300acf1f7f..16766278f658 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -179,9 +179,9 @@ Enter a title for the set of changes with some explanation of what you've done. Mention anything you'd like particular attention for - such as a complicated change or some code you are not happy with. -If you don't think your request is ready to be merged, make a -:ref:`draft pull request ` and state what aspects you want to have -feedback on. This is a good way of getting some preliminary code review. +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 `_. diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index e7e3ceba8f95..a02b52ad5a38 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -12,7 +12,7 @@ We value contributions from people with all levels of experience. In particular, if this is your first PR not everything has to be perfect. We'll guide you through the PR process. Nevertheless, please try to follow our guidelines as well as you can to help make the PR process quick and smooth. If your pull request is -incomplete or a work-in-progress, please mark it as a :ref:`draft pull request ` +incomplete or a work-in-progress, please mark it as a `draft pull requests `_ on GitHub and specify what feedback from the developers would be helpful. Please be patient with reviewers. We try our best to respond quickly, but we have @@ -109,32 +109,15 @@ Workflow * The PR should :ref:`target the main branch `. * Tag with descriptive :ref:`labels `. * Set the :ref:`milestone `. -* :ref:`Review ` the contents. -* Approve if all of the above topics are handled. * Keep an eye on the :ref:`number of commits `. -* :ref:`Merge ` if a :ref:`sufficient number of approvals ` is reached. +* Approve if all of the above topics are handled. +* :ref:`Merge ` if a sufficient number of approvals is reached. .. _pr-guidelines-details: Detailed guidelines =================== -.. _draft-pr: - -Draft PRs ---------- - -Authors may create a `draft PR`_ (or change to draft status later) if the code -is not yet ready for a regular full review. Typical use cases are posting code -as a basis for discussion or signalling that you intend to rework the code as -a result of feedback. Authors should clearly communicate why the PR has draft -status and what needs to be done to make it ready for review. In particular, -they should explicitly ask for targeted feedback if needed. By default, -reviewers will not look at the code of a draft PR and only respond to specific -questions by the author. - -.. _draft PR: https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests - .. _pr-documentation: Documentation @@ -191,27 +174,10 @@ All Pull Requests should target the main branch. The milestone tag triggers an :ref:`automatic backport ` for milestones which have a corresponding branch. -.. _pr-review: - -Review ------- - -* Do not let perfect be the enemy of the good, particularly for - documentation or example PRs. If you find yourself making many - small suggestions, either open a PR against the original branch, - push changes to the contributor branch, or merge the PR and then - open a new PR against upstream. - -* If you push to a contributor branch leave a comment explaining what - you did, ex "I took the liberty of pushing a small clean-up PR to - your branch, thanks for your work.". If you are going to make - substantial changes to the code or intent of the PR please check - with the contributor first. - -.. _pr-approval: +.. _pr-merging: -Approval --------- +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 @@ -247,6 +213,11 @@ Some explicit rules following from this: A core dev should only champion one PR at a time and we should try to keep the flow of championed PRs reasonable. +After giving the last required approval, the author of the approval should +merge the PR. PR authors should not self-merge except for when another reviewer +explicitly allows it (e.g., "Approve modulo CI passing, may self merge when +green", or "Take or leave the comments. You may self merge".). + .. _pr-automated-tests: Automated tests @@ -254,15 +225,6 @@ Automated tests Before being merged, a PR should pass the :ref:`automated-tests`. If you are unsure why a test is failing, ask on the PR or in our :ref:`communication-channels` -.. _pr-merging: - -Merging -------- -After giving the last required :ref:`approval `, the author of the -approval should merge the PR. PR authors should not self-merge except for when -another reviewer explicitly allows it (e.g., "Approve modulo CI passing, may -self-merge when green", or "Take or leave the comments. You may self merge".). - .. _pr-squashing: Number of commits and squashing @@ -274,6 +236,19 @@ Number of commits and squashing about it is to eliminate binary files (ex multiple test image re-generations) and to remove upstream merges. +* Do not let perfect be the enemy of the good, particularly for + documentation or example PRs. If you find yourself making many + small suggestions, either open a PR against the original branch, + push changes to the contributor branch, or merge the PR and then + open a new PR against upstream. + +* If you push to a contributor branch leave a comment explaining what + you did, ex "I took the liberty of pushing a small clean-up PR to + your branch, thanks for your work.". If you are going to make + substantial changes to the code or intent of the PR please check + with the contributor first. + + .. _branches_and_backports: Branches and backports diff --git a/doc/release/next_whats_new/scroll_to_zoom.rst b/doc/release/next_whats_new/scroll_to_zoom.rst deleted file mode 100644 index bafa312c32a5..000000000000 --- a/doc/release/next_whats_new/scroll_to_zoom.rst +++ /dev/null @@ -1,8 +0,0 @@ -Zooming using mouse wheel -~~~~~~~~~~~~~~~~~~~~~~~~~ - -``Ctrl+MouseWheel`` can be used to zoom in the plot windows. - -The zoom focusses on the mouse pointer, and keeps the aspect ratio of the axes. - -Zooming is currently only supported on rectilinear Axes. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 0db8bdd6f643..53dfbc2a2e91 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -38,7 +38,7 @@ from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.transforms import _ScaledRotation - +import datetime _log = logging.getLogger(__name__) @@ -8878,8 +8878,18 @@ def violinplot(self, dataset, positions=None, vert=None, .Axes.violin : Draw a violin from pre-computed statistics. boxplot : Draw a box and whisker plot. """ - vpstats = cbook.violin_stats(dataset, ("GaussianKDE", bw_method), - points=points, quantiles=quantiles) + + def _kde_method(X, coords): + # Unpack in case of e.g. Pandas or xarray object + X = cbook._unpack_to_numpy(X) + # fallback gracefully if the vector contains only one value + if np.all(X[0] == X): + return (X[0] == coords).astype(float) + kde = mlab.GaussianKDE(X, bw_method) + return kde.evaluate(coords) + + vpstats = cbook.violin_stats(dataset, _kde_method, points=points, + quantiles=quantiles) return self.violin(vpstats, positions=positions, vert=vert, orientation=orientation, widths=widths, showmeans=showmeans, showextrema=showextrema, @@ -9041,6 +9051,17 @@ def violin(self, vpstats, positions=None, vert=None, elif len(positions) != N: raise ValueError(datashape_message.format("positions")) + #Checkif positions are datetime-like, widths must be timedelta-like. + if any(isinstance(p, (datetime.datetime, datetime.date)) for p in positions): + _widths = widths if not np.isscalar(widths) else [widths] * N + if any(not isinstance(w, (datetime.timedelta, np.timedelta64)) + for w in _widths): + raise TypeError( + "If positions are datetime/date values, pass widths as " + "datetime.timedelta (e.g., datetime.timedelta(days=10)) " + "or numpy.timedelta64." + ) + # Validate widths if np.isscalar(widths): widths = [widths] * N diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index e08d173744ad..42f6e51786bf 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1736,8 +1736,6 @@ def get_adjustable(self): Return whether the Axes will adjust its physical dimension ('box') or its data limits ('datalim') to achieve the desired aspect ratio. - Newly created Axes default to 'box'. - See Also -------- matplotlib.axes.Axes.set_adjustable @@ -1763,8 +1761,6 @@ def set_adjustable(self, adjustable, share=False): See Also -------- - matplotlib.axes.Axes.get_adjustable - Return the current value of *adjustable*. matplotlib.axes.Axes.set_aspect For a description of aspect handling. @@ -1796,7 +1792,7 @@ def set_adjustable(self, adjustable, share=False): "Axes which override 'get_data_ratio'") for ax in axs: ax._adjustable = adjustable - ax.stale = True + self.stale = True def get_box_aspect(self): """ diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 7560db80d2c1..8107471955fe 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2574,51 +2574,6 @@ def button_press_handler(event, canvas=None, toolbar=None): toolbar.forward() -def scroll_handler(event, canvas=None, toolbar=None): - ax = event.inaxes - if ax is None: - return - if ax.name != "rectilinear": - # zooming is currently only supported on rectilinear axes - return - - if toolbar is None: - toolbar = (canvas or event.canvas).toolbar - - if toolbar is None: - # technically we do not need a toolbar, but until wheel zoom was - # introduced, any interactive modification was only possible through - # the toolbar tools. For now, we keep the restriction that a toolbar - # is required for interactive navigation. - return - - if event.key == "control": # zoom towards the mouse position - toolbar.push_current() - - xmin, xmax = ax.get_xlim() - ymin, ymax = ax.get_ylim() - (xmin, ymin), (xmax, ymax) = ax.transScale.transform( - [(xmin, ymin), (xmax, ymax)]) - - # mouse position in scaled (e.g., log) data coordinates - x, y = ax.transScale.transform((event.xdata, event.ydata)) - - scale_factor = 0.85 ** event.step - new_xmin = x - (x - xmin) * scale_factor - new_xmax = x + (xmax - x) * scale_factor - new_ymin = y - (y - ymin) * scale_factor - new_ymax = y + (ymax - y) * scale_factor - - inv_scale = ax.transScale.inverted() - (new_xmin, new_ymin), (new_xmax, new_ymax) = inv_scale.transform( - [(new_xmin, new_ymin), (new_xmax, new_ymax)]) - - ax.set_xlim(new_xmin, new_xmax) - ax.set_ylim(new_ymin, new_ymax) - - ax.figure.canvas.draw_idle() - - class NonGuiException(Exception): """Raised when trying show a figure in a non-GUI backend.""" pass @@ -2698,14 +2653,11 @@ def __init__(self, canvas, num): self.key_press_handler_id = None self.button_press_handler_id = None - self.scroll_handler_id = None if rcParams['toolbar'] != 'toolmanager': self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', key_press_handler) self.button_press_handler_id = self.canvas.mpl_connect( 'button_press_event', button_press_handler) - self.scroll_handler_id = self.canvas.mpl_connect( - 'scroll_event', scroll_handler) self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 7a2b28262249..c65d39415472 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -407,11 +407,6 @@ def button_press_handler( canvas: FigureCanvasBase | None = ..., toolbar: NavigationToolbar2 | None = ..., ) -> None: ... -def scroll_handler( - event: MouseEvent, - canvas: FigureCanvasBase | None = ..., - toolbar: NavigationToolbar2 | None = ..., -) -> None: ... class NonGuiException(Exception): ... @@ -420,7 +415,6 @@ class FigureManagerBase: num: int | str key_press_handler_id: int | None button_press_handler_id: int | None - scroll_handler_id: int | None toolmanager: ToolManager | None toolbar: NavigationToolbar2 | ToolContainerBase | None def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ... diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 4e05119aa0f6..888f5a770f5d 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -18,13 +18,6 @@ # :raises ValueError: If module/version is already loaded, already # required, or unavailable. gi.require_version("Gtk", "3.0") - # Also require GioUnix to avoid PyGIWarning when Gio is imported - # GioUnix is platform-specific and may not be available on all systems - try: - gi.require_version("GioUnix", "2.0") - except ValueError: - # GioUnix is not available on this platform, which is fine - pass except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index a45fa0bc490f..cd38968779ed 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -17,13 +17,6 @@ # :raises ValueError: If module/version is already loaded, already # required, or unavailable. gi.require_version("Gtk", "4.0") - # Also require GioUnix to avoid PyGIWarning when Gio is imported - # GioUnix is platform-specific and may not be available on all systems - try: - gi.require_version("GioUnix", "2.0") - except ValueError: - # GioUnix is not available on this platform, which is fine - pass except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index d63808eb3925..a75a8a86eb92 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1035,10 +1035,11 @@ def _embedTeXFont(self, dvifont): fontdict['Encoding'] = self._generate_encoding(encoding) fc = fontdict['FirstChar'] = min(encoding.keys(), default=0) lc = fontdict['LastChar'] = max(encoding.keys(), default=255) + # Convert glyph widths from TeX 12.20 fixed point to 1/1000 text space units - font_metrics = dvifont._metrics - widths = [(1000 * glyph_metrics.tex_width) >> 20 - if (glyph_metrics := font_metrics.get_metrics(char)) else 0 + tfm = dvifont._tfm + widths = [(1000 * metrics.tex_width) >> 20 + if (metrics := tfm.get_metrics(char)) else 0 for char in range(fc, lc + 1)] fontdict['Widths'] = widthsObject = self.reserveObject('glyph widths') self.writeObject(widthsObject, widths) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index f20ed008f147..a09780965b0c 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -29,7 +29,7 @@ from numpy import VisibleDeprecationWarning import matplotlib -from matplotlib import _api, _c_internal_utils, mlab +from matplotlib import _api, _c_internal_utils class _ExceptionInfo: @@ -43,20 +43,16 @@ class _ExceptionInfo: users and result in incorrect tracebacks. """ - def __init__(self, cls, *args, notes=None): + def __init__(self, cls, *args): self._cls = cls self._args = args - self._notes = notes if notes is not None else [] @classmethod def from_exception(cls, exc): - return cls(type(exc), *exc.args, notes=getattr(exc, "__notes__", [])) + return cls(type(exc), *exc.args) def to_exception(self): - exc = self._cls(*self._args) - for note in self._notes: - exc.add_note(note) - return exc + return self._cls(*self._args) def _get_running_interactive_framework(): @@ -1434,7 +1430,7 @@ def _reshape_2D(X, name): return result -def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None): +def violin_stats(X, method, points=100, quantiles=None): """ Return a list of dictionaries of data which can be used to draw a series of violin plots. @@ -1453,23 +1449,11 @@ def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None) Sample data that will be used to produce the gaussian kernel density estimates. Must have 2 or fewer dimensions. - method : (name, bw_method) or callable, + method : callable The method used to calculate the kernel density estimate for each - column of data. Valid values: - - - a tuple of the form ``(name, bw_method)`` where *name* currently must - always be ``"GaussianKDE"`` and *bw_method* is the method used to - calculate the estimator bandwidth. Supported values are 'scott', - 'silverman' or a float or a callable. If a float, this will be used - directly as `!kde.factor`. If a callable, it should take a - `matplotlib.mlab.GaussianKDE` instance as its only parameter and - return a float. - - - a callable with the signature :: - - def method(data: ndarray, coords: ndarray) -> ndarray - - It should return the KDE of *data* evaluated at *coords*. + column of data. When called via ``method(v, coords)``, it should + return a vector of the values of the KDE evaluated at the values + specified in coords. points : int, default: 100 Defines the number of points to evaluate each of the gaussian kernel @@ -1497,20 +1481,6 @@ def method(data: ndarray, coords: ndarray) -> ndarray - max: The maximum value for this column of data. - quantiles: The quantile values for this column of data. """ - if isinstance(method, tuple): - name, bw_method = method - if name != "GaussianKDE": - raise ValueError(f"Unknown KDE method name {name!r}. The only supported " - 'named method is "GaussianKDE"') - - def _kde_method(x, coords): - # fallback gracefully if the vector contains only one value - if np.all(x[0] == x): - return (x[0] == coords).astype(float) - kde = mlab.GaussianKDE(x, bw_method) - return kde.evaluate(coords) - - method = _kde_method # List of dictionaries describing each of the violins. vpstats = [] diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index f7959a6fd0bb..ad14841463e8 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -133,10 +133,7 @@ ls_mapper_r: dict[str, str] def contiguous_regions(mask: ArrayLike) -> list[np.ndarray]: ... def is_math_text(s: str) -> bool: ... def violin_stats( - X: ArrayLike, - method: tuple[Literal["GaussianKDE"], Literal["scott", "silverman"] | float | Callable] | Callable = ..., - points: int = ..., - quantiles: ArrayLike | None = ... + X: ArrayLike, method: Callable, points: int = ..., quantiles: ArrayLike | None = ... ) -> list[dict[str, Any]]: ... def pts_to_prestep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... def pts_to_poststep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 679f368bae30..f60c8eb48134 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1322,7 +1322,7 @@ class ListedColormap(Colormap): "and will be removed in %(removal)s. Please ensure the list " "of passed colors is the required length instead." ) - def __init__(self, colors, name='unnamed', N=None, *, + def __init__(self, colors, name='from_list', N=None, *, bad=None, under=None, over=None): if N is None: self.colors = colors diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 702543f9db26..9e8b6a5facf5 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -26,15 +26,12 @@ import subprocess import sys from collections import namedtuple -from functools import cache, cached_property, lru_cache, partial, wraps +from functools import cache, lru_cache, partial, wraps from pathlib import Path -import fontTools.agl import numpy as np -import matplotlib as mpl from matplotlib import _api, cbook, font_manager -from matplotlib.ft2font import LoadFlags _log = logging.getLogger(__name__) @@ -70,16 +67,43 @@ class Text(namedtuple('Text', 'x y font glyph width')): """ A glyph in the dvi file. - In order to render the glyph, load the glyph at index ``text.index`` - from the font at ``text.font.resolve_path()`` with size ``text.font.size``, - warped with ``text.font.effects``, then draw it at position - ``(text.x, text.y)``. - - ``text.glyph`` is the glyph number actually stored in the dvi file (whose - interpretation depends on the font). ``text.width`` is the glyph width in - dvi units. + The *x* and *y* attributes directly position the glyph. The *font*, + *glyph*, and *width* attributes are kept public for back-compatibility, + but users wanting to draw the glyph themselves are encouraged to instead + load the font specified by `font_path` at `font_size`, warp it with the + effects specified by `font_effects`, and load the glyph at the FreeType + glyph `index`. """ + def _get_pdftexmap_entry(self): + return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] + + @property + def font_path(self): + """The `~pathlib.Path` to the font for this glyph.""" + psfont = self._get_pdftexmap_entry() + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + return Path(psfont.filename) + + @property + def font_size(self): + """The font size.""" + return self.font.size + + @property + def font_effects(self): + """ + The "font effects" dict for this glyph. + + This dict contains the values for this glyph of SlantFont and + ExtendFont (if any), read off :file:`pdftex.map`. + """ + return self._get_pdftexmap_entry().effects + @property def index(self): """ @@ -88,57 +112,25 @@ def index(self): # See DviFont._index_dvi_to_freetype for details on the index mapping. return self.font._index_dvi_to_freetype(self.glyph) - font_path = property(lambda self: self.font.resolve_path()) - font_size = property(lambda self: self.font.size) - font_effects = property(lambda self: self.font.effects) - - @property # To be deprecated together with font_path, font_size, font_effects. + @property # To be deprecated together with font_size, font_effects. def glyph_name_or_index(self): """ - The glyph name, the native charmap glyph index, or the raw glyph index. - - If the font is a TrueType file (which can currently only happen for - DVI files generated by xetex or luatex), then this number is the raw - index of the glyph, which can be passed to FT_Load_Glyph/load_glyph. - - Otherwise, the font is a PostScript font. For such fonts, if - :file:`pdftex.map` specifies an encoding for this glyph's font, - that is a mapping of glyph indices to Adobe glyph names; which - is used by this property to convert dvi numbers to glyph names. - Callers can then convert glyph names to glyph indices (with - FT_Get_Name_Index/get_name_index), and load the glyph using - FT_Load_Glyph/load_glyph. - - If :file:`pdftex.map` specifies no encoding for a PostScript font, - this number is an index to the font's "native" charmap; glyphs should - directly load using FT_Load_Char/load_char after selecting the native - charmap. + Either the glyph name or the native charmap glyph index. + + If :file:`pdftex.map` specifies an encoding for this glyph's font, that + is a mapping of glyph indices to Adobe glyph names; use it to convert + dvi indices to glyph names. Callers can then convert glyph names to + glyph indices (with FT_Get_Name_Index/get_name_index), and load the + glyph using FT_Load_Glyph/load_glyph. + + If :file:`pdftex.map` specifies no encoding, the indices directly map + to the font's "native" charmap; glyphs should directly load using + FT_Load_Char/load_char after selecting the native charmap. """ - # The last section is only true on luatex since luaotfload 3.23; this - # must be checked by the code generated by texmanager. (luaotfload's - # docs states "No one should rely on the mapping between DVI character - # codes and font glyphs [prior to v3.15] unless they tightly - # control all involved versions and are deeply familiar with the - # implementation", but a further mapping bug was fixed in luaotfload - # commit 8f2dca4, first included in v3.23). entry = self._get_pdftexmap_entry() return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) - def _as_unicode_or_name(self): - if self.font.subfont: - raise NotImplementedError("Indexing TTC fonts is not supported yet") - path = self.font.resolve_path() - if path.name.lower().endswith("pk"): - # PK fonts have no encoding information; report glyphs as ASCII but - # with a "?" to indicate that this is just a guess. - return (f"{chr(self.glyph)}?" if chr(self.glyph).isprintable() else - f"pk{self.glyph:#02x}") - face = font_manager.get_font(path) - glyph_name = face.get_glyph_name(self.index) - glyph_str = fontTools.agl.toUnicode(glyph_name) - return glyph_str or glyph_name - # Opcode argument parsing # @@ -416,7 +408,7 @@ def _put_char_real(self, char): scale = font._scale for x, y, f, g, w in font._vf[char].text: newf = DviFont(scale=_mul1220(scale, f._scale), - metrics=f._metrics, texname=f.texname, vf=f._vf) + tfm=f._tfm, texname=f.texname, vf=f._vf) self.text.append(Text(self.h + _mul1220(x, scale), self.v + _mul1220(y, scale), newf, g, newf._width_of(g))) @@ -512,21 +504,10 @@ 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:] - if fontname.startswith(b"[") and c == 0x4c756146: # c == "LuaF" - # See https://chat.stackexchange.com/rooms/106428 (and also - # https://tug.org/pipermail/dvipdfmx/2021-January/000168.html). - # AFAICT luatex's dvi drops info re: OpenType variation-axis values. - self.fonts[k] = DviFont.from_luatex(s, n) - return - fontname = fontname.decode("ascii") + fontname = n[-l:].decode('ascii') try: tfm = _tfmfile(fontname) except FileNotFoundError as exc: - if fontname.startswith("[") and fontname.endswith(";") and c == 0: - exc.add_note( - "This dvi file was likely generated with a too-old " - "version of luaotfload; luaotfload 3.23 is required.") # Explicitly allow defining missing fonts for Vf support; we only # register an error when trying to load a glyph from a missing font # and throw that error in Dvi._read. For Vf, _finalize_packet @@ -540,12 +521,12 @@ def _fnt_def_real(self, k, c, s, d, a, l): vf = _vffile(fontname) except FileNotFoundError: vf = None - self.fonts[k] = DviFont(scale=s, metrics=tfm, texname=n, vf=vf) + self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file - if i not in [2, 7]: # 2: pdftex, luatex; 7: xetex + if i != 2: raise ValueError(f"Unknown dvi format {i}") if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") @@ -566,66 +547,13 @@ def _post(self, _): # TODO: actually read the postamble and finale? # currently post_post just triggers closing the file - @_dispatch(249, args=()) - def _post_post(self): - raise NotImplementedError - - @_dispatch(250, args=()) - def _begin_reflect(self): - raise NotImplementedError - - @_dispatch(251, args=()) - def _end_reflect(self): + @_dispatch(249) + def _post_post(self, _): raise NotImplementedError - @_dispatch(252, args=()) - def _define_native_font(self): - k = self._read_arg(4, signed=False) - s = self._read_arg(4, signed=False) - flags = self._read_arg(2, signed=False) - l = self._read_arg(1, signed=False) - n = self.file.read(l) - i = self._read_arg(4, signed=False) - effects = {} - if flags & 0x0200: - effects["rgba"] = [self._read_arg(1, signed=False) for _ in range(4)] - if flags & 0x1000: - effects["extend"] = self._read_arg(4, signed=True) / 65536 - if flags & 0x2000: - effects["slant"] = self._read_arg(4, signed=True) / 65536 - if flags & 0x4000: - effects["embolden"] = self._read_arg(4, signed=True) / 65536 - self.fonts[k] = DviFont.from_xetex(s, n, i, effects) - - @_dispatch(253, args=()) - def _set_glyphs(self): - w = self._read_arg(4, signed=False) - k = self._read_arg(2, signed=False) - xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] - g = [self._read_arg(2, signed=False) for _ in range(k)] - font = self.fonts[self.f] - for i in range(k): - self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], - font, g[i], font._width_of(g[i]))) - self.h += w - - @_dispatch(254, args=()) - def _set_text_and_glyphs(self): - l = self._read_arg(2, signed=False) - t = self.file.read(2 * l) # utf16 - w = self._read_arg(4, signed=False) - k = self._read_arg(2, signed=False) - xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] - g = [self._read_arg(2, signed=False) for _ in range(k)] - font = self.fonts[self.f] - for i in range(k): - self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], - font, g[i], font._width_of(g[i]))) - self.h += w - - @_dispatch(255) - def _malformed(self, raw): - raise ValueError("unknown command: byte 255") + @_dispatch(min=250, max=255) + def _malformed(self, offset): + raise ValueError(f"unknown command: byte {250 + offset}") class DviFont: @@ -643,10 +571,10 @@ class DviFont: ---------- scale : float Factor by which the font is scaled from its natural size. - metrics : Tfm | TtfMetrics + tfm : Tfm TeX font metrics for this font texname : bytes - Name of the font as used internally in the DVI file, as an ASCII + Name of the font as used internally by TeX and friends, as an ASCII bytestring. This is usually very different from any external font names; `PsfontsMap` can be used to find the external name of the font. vf : Vf @@ -662,54 +590,17 @@ class DviFont: Size of the font in Adobe points, converted from the slightly smaller TeX points. """ + __slots__ = ('texname', 'size', '_scale', '_vf', '_tfm', '_encoding') - def __init__(self, scale, metrics, texname, vf): + def __init__(self, scale, tfm, texname, vf): _api.check_isinstance(bytes, texname=texname) self._scale = scale - self._metrics = metrics + self._tfm = tfm self.texname = texname self._vf = vf - self._path = None + self.size = scale * (72.0 / (72.27 * 2**16)) self._encoding = None - @classmethod - def from_luatex(cls, scale, texname): - path_b, sep, rest = texname[1:].rpartition(b"]") - if not (texname.startswith(b"[") and sep and rest[:1] in [b"", b":"]): - raise ValueError(f"Invalid modern font name: {texname}") - # utf8 on Windows, not utf16! - path = path_b.decode("utf8") if os.name == "nt" else os.fsdecode(path_b) - subfont = 0 - effects = {} - if rest[1:]: - for kv in rest[1:].decode("ascii").split(";"): - key, val = kv.split("=", 1) - if key == "index": - subfont = val - elif key in ["embolden", "slant", "extend"]: - effects[key] = int(val) / 65536 - else: - _log.warning("Ignoring invalid key-value pair: %r", kv) - metrics = TtfMetrics(path) - font = cls(scale, metrics, texname, vf=None) - font._path = Path(path) - font.subfont = subfont - font.effects = effects - return font - - @classmethod - def from_xetex(cls, scale, texname, subfont, effects): - # utf8 on Windows, not utf16! - path = texname.decode("utf8") if os.name == "nt" else os.fsdecode(texname) - metrics = TtfMetrics(path) - font = cls(scale, metrics, b"[" + texname + b"]", vf=None) - font._path = Path(path) - font.subfont = subfont - font.effects = effects - return font - - size = property(lambda self: self._scale * (72.0 / (72.27 * 2**16))) - widths = _api.deprecated("3.11")(property(lambda self: [ (1000 * self._tfm.width.get(char, 0)) >> 20 for char in range(max(self._tfm.width, default=-1) + 1)])) @@ -738,7 +629,7 @@ def __repr__(self): def _width_of(self, char): """Width of char in dvi units.""" - metrics = self._metrics.get_metrics(char) + metrics = self._tfm.get_metrics(char) if metrics is None: _log.debug('No width for char %d in font %s.', char, self.texname) return 0 @@ -746,7 +637,7 @@ def _width_of(self, char): def _height_depth_of(self, char): """Height and depth of char in dvi units.""" - metrics = self._metrics.get_metrics(char) + metrics = self._tfm.get_metrics(char) if metrics is None: _log.debug('No metrics for char %d in font %s', char, self.texname) return [0, 0] @@ -763,55 +654,26 @@ def _height_depth_of(self, char): hd[-1] = 0 return hd - def resolve_path(self): - if self._path is None: - fontmap = PsfontsMap(find_tex_file("pdftex.map")) - try: - psfont = fontmap[self.texname] - except LookupError as exc: - try: - find_tex_file(f"{self.texname.decode('ascii')}.mf") - except FileNotFoundError: - raise exc from None - else: - self._path = Path(find_tex_file( - f"{self.texname.decode('ascii')}.600pk")) - else: - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - self._path = Path(psfont.filename) - return self._path - - @cached_property - def subfont(self): - return 0 - - @cached_property - def effects(self): - if self.resolve_path().match("*.600pk"): - return {} - return PsfontsMap(find_tex_file("pdftex.map"))[self.texname].effects - def _index_dvi_to_freetype(self, idx): """Convert dvi glyph indices to FreeType ones.""" # Glyphs indices stored in the dvi file map to FreeType glyph indices # (i.e., which can be passed to FT_Load_Glyph) in various ways: - # - for xetex & luatex "native fonts", dvi indices are directly equal - # to FreeType indices. # - if pdftex.map specifies an ".enc" file for the font, that file maps # dvi indices to Adobe glyph names, which can then be converted to # FreeType glyph indices with FT_Get_Name_Index. # - if no ".enc" file is specified, then the font must be a Type 1 # font, and dvi indices directly index into the font's CharStrings # vector. - if self.texname.startswith(b"["): - return idx + # - (xetex & luatex, currently unsupported, can also declare "native + # fonts", for which dvi indices are equal to FreeType indices.) if self._encoding is None: - face = font_manager.get_font(self.resolve_path()) psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname] + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + face = font_manager.get_font(psfont.filename) if psfont.encoding: self._encoding = [face.get_name_index(name) for name in _parse_enc(psfont.encoding)] @@ -1020,27 +882,6 @@ def get_metrics(self, idx): property(lambda self: {c: m.tex_depth for c, m in self._glyph_metrics})) -class TtfMetrics: - def __init__(self, filename): - self._face = font_manager.get_font(filename, hinting_factor=1) - - def get_metrics(self, idx): - # _mul1220 uses a truncating bitshift for compatibility with dvitype. - # When upem is 2048 the conversion to 12.20 is exact, but when - # upem is 1000 (e.g. lmroman10-regular.otf) the metrics themselves - # are not exactly representable as 12.20 fp. Manual testing via - # \sbox0{x}\count0=\wd0\typeout{\the\count0} suggests that metrics - # are rounded (not truncated) after conversion to 12.20 and before - # multiplication by the scale. - upem = self._face.units_per_EM # Usually 2048 or 1000. - g = self._face.load_glyph(idx, LoadFlags.NO_SCALE) - return TexMetrics( - tex_width=round(g.horiAdvance / upem * 2**20), - tex_height=round(g.horiBearingY / upem * 2**20), - tex_depth=round((g.height - g.horiBearingY) / upem * 2**20), - ) - - PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') @@ -1253,12 +1094,9 @@ def __new__(cls): def _new_proc(self): return subprocess.Popen( - ["luatex", "--luaonly", str(cbook._get_data_path("kpsewhich.lua"))], - # mktexpk logs to stderr; suppress that. - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - # Store generated pk fonts in our own cache. - env={"MT_VARTEXFONTS": str(Path(mpl.get_cachedir(), "vartexfonts")), - **os.environ}) + ["luatex", "--luaonly", + str(cbook._get_data_path("kpsewhich.lua"))], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) def search(self, filename): if self._proc.poll() is not None: # Dead, restart it. @@ -1310,16 +1148,13 @@ def find_tex_file(filename): kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, 'encoding': 'utf-8'} else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'env': {**os.environ}, - 'encoding': sys.getfilesystemencoding(), + kwargs = {'encoding': sys.getfilesystemencoding(), 'errors': 'surrogateescape'} - kwargs['env'].update( - MT_VARTEXFONTS=str(Path(mpl.get_cachedir(), "vartexfonts"))) try: - path = cbook._check_and_log_subprocess( - ['kpsewhich', '-mktex=pk', filename], _log, **kwargs, - ).rstrip('\n') + path = (cbook._check_and_log_subprocess(['kpsewhich', filename], + _log, **kwargs) + .rstrip('\n')) except (FileNotFoundError, RuntimeError): path = None @@ -1344,6 +1179,10 @@ def _fontfile(cls, suffix, texname): import itertools from argparse import ArgumentParser + import fontTools.agl + + from matplotlib.ft2font import FT2Font + parser = ArgumentParser() parser.add_argument("filename") parser.add_argument("dpi", nargs="?", type=float, default=None) @@ -1353,22 +1192,22 @@ def _print_fields(*args): print(" ".join(map("{:>11}".format, args))) with Dvi(args.filename, args.dpi) as dvi: + fontmap = PsfontsMap(find_tex_file('pdftex.map')) for page in dvi: print(f"=== NEW PAGE === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") print("--- GLYPHS ---") - for font, group in itertools.groupby(page.text, lambda text: text.font): - font_name = (font.texname.decode("utf8") if os.name == "nt" - else os.fsdecode(font.texname)) - if isinstance(font._metrics, Tfm): - print(f"font: {font_name} at {font.resolve_path()}") - else: - print(f"font: {font_name}") - print(f"scale: {font._scale / 2 ** 20}") + for font, group in itertools.groupby( + page.text, lambda text: text.font): + psfont = fontmap[font.texname] + fontpath = psfont.filename + print(f"font: {font.texname.decode('latin-1')} " + f"(scale: {font._scale / 2 ** 20}) at {fontpath}") + face = FT2Font(fontpath) _print_fields("x", "y", "glyph", "chr", "w") for text in group: - _print_fields(text.x, text.y, text.glyph, - text._as_unicode_or_name(), text.width) + glyph_str = fontTools.agl.toUnicode(face.get_glyph_name(text.index)) + _print_fields(text.x, text.y, text.glyph, glyph_str, text.width) if page.boxes: print("--- BOXES ---") _print_fields("x", "y", "h", "w") diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 1c24ff1c28a9..82c0238d39d1 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -58,28 +58,16 @@ class Dvi: class DviFont: texname: bytes + size: float def __init__( - self, scale: float, metrics: Tfm | TtfMetrics, texname: bytes, vf: Vf | None + self, scale: float, tfm: Tfm, texname: bytes, vf: Vf | None ) -> None: ... - @classmethod - def from_luatex(cls, scale: float, texname: bytes) -> DviFont: ... - @classmethod - def from_xetex( - cls, scale: float, texname: bytes, subfont: int, effects: dict[str, float] - ) -> DviFont: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @property - def size(self) -> float: ... - @property def widths(self) -> list[int]: ... @property def fname(self) -> str: ... - def resolve_path(self) -> Path: ... - @property - def subfont(self) -> int: ... - @property - def effects(self) -> dict[str, float]: ... class Vf(Dvi): def __init__(self, filename: str | os.PathLike) -> None: ... @@ -105,10 +93,6 @@ class Tfm: @property def depth(self) -> dict[int, int]: ... -class TtfMetrics: - def __init__(self, filename: str | os.PathLike) -> None: ... - def get_metrics(self, idx: int) -> TexMetrics: ... - class PsFont(NamedTuple): texname: bytes psname: bytes diff --git a/lib/matplotlib/mpl-data/kpsewhich.lua b/lib/matplotlib/mpl-data/kpsewhich.lua index dc526effeebe..8e9172a45082 100644 --- a/lib/matplotlib/mpl-data/kpsewhich.lua +++ b/lib/matplotlib/mpl-data/kpsewhich.lua @@ -1,4 +1,3 @@ -- see dviread._LuatexKpsewhich kpse.set_program_name("latex") -kpse.init_prog("", 600, "ljfour") -while true do print(kpse.lookup(io.read():gsub("\r", ""), {mktexpk=true})); io.flush(); end +while true do print(kpse.lookup(io.read():gsub("\r", ""))); io.flush(); end diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index df693c57d272..91c510ca7060 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -144,7 +144,7 @@ length in y direction = $\\frac{v}{\\mathrm{scale}} \\mathrm{scale_unit}$ - For example, ``(u, v) = (0.5, 0)`` with ``scale=10, scale_units="width"`` results + 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. diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index eff079efe887..904ee5d73db4 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -54,7 +54,7 @@ def setup(): def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, stderr=None, check=False, text=True, - capture_output=False, **kwargs): + capture_output=False): """ Create and run a subprocess. @@ -97,7 +97,7 @@ def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, command, env=env, timeout=timeout, check=check, stdout=stdout, stderr=stderr, - text=text, **kwargs + text=text ) except BlockingIOError: if sys.platform == "cygwin": diff --git a/lib/matplotlib/testing/__init__.pyi b/lib/matplotlib/testing/__init__.pyi index accf973615fa..7763cb6a9769 100644 --- a/lib/matplotlib/testing/__init__.pyi +++ b/lib/matplotlib/testing/__init__.pyi @@ -16,7 +16,6 @@ def subprocess_run_for_testing( *, text: Literal[True], capture_output: bool = ..., - **kwargs, ) -> subprocess.CompletedProcess[str]: ... @overload def subprocess_run_for_testing( @@ -28,7 +27,6 @@ def subprocess_run_for_testing( check: bool = ..., text: Literal[False] = ..., capture_output: bool = ..., - **kwargs, ) -> subprocess.CompletedProcess[bytes]: ... @overload def subprocess_run_for_testing( @@ -40,7 +38,6 @@ def subprocess_run_for_testing( check: bool = ..., text: bool = ..., capture_output: bool = ..., - **kwargs, ) -> subprocess.CompletedProcess[bytes] | subprocess.CompletedProcess[str]: ... def subprocess_run_helper( func: Callable[[], None], diff --git a/lib/matplotlib/tests/baseline_images/dviread/lualatex.json b/lib/matplotlib/tests/baseline_images/dviread/lualatex.json deleted file mode 100644 index 8f2d95017ec7..000000000000 --- a/lib/matplotlib/tests/baseline_images/dviread/lualatex.json +++ /dev/null @@ -1 +0,0 @@ -[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8072180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8473140, 4128768, "c", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8833460, 4128768, ".", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738721, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920911, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json b/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json deleted file mode 100644 index 4754b722aa58..000000000000 --- a/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json +++ /dev/null @@ -1 +0,0 @@ -[{"text": [[5046272, 4128768, "A", "cmr10.pfb", 9.96, {}], [5756246, 4128768, "L", "cmr10.pfb", 9.96, {}], [5929917, 3994421, "A", "cmr7.pfb", 6.97, {}], [6218464, 4128768, "T", "cmr10.pfb", 9.96, {}], [6582530, 4269852, "E", "cmr10.pfb", 9.96, {}], [6946620, 4128768, "X", "cmr10.pfb", 9.96, {}], [7656594, 4128768, "d", "cmr10.pfb", 9.96, {}], [8020684, 4128768, "o", "cmr10.pfb", 9.96, {}], [8366570, 4128768, "c", "cmr10.pfb", 9.96, {}], [8657841, 4128768, ".", "cmr10.pfb", 9.96, {}]], "boxes": []}, {"text": [[13686591, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13717140, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355327, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406754, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010688, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937658, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480727, 6804696, "s", "cmr10.pfb", 9.96, {}], [14739230, 6804696, "i", "cmr10.pfb", 9.96, {}], [14921275, 6804696, "n", "cmr10.pfb", 9.96, {}], [15394589, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847788, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239184, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642411, 6355152, "d", "cmr10.pfb", 9.96, {}], [17006501, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686591, 5130818, 26213, 284106], [14480727, 6204418, 26213, 1288418]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.dvi b/lib/matplotlib/tests/baseline_images/dviread/test.dvi new file mode 100644 index 000000000000..93751ffdcba0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/test.dvi differ diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.json b/lib/matplotlib/tests/baseline_images/dviread/test.json new file mode 100644 index 000000000000..0809cb9531f1 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/test.json @@ -0,0 +1,94 @@ +[ + { + "text": [ + [5046272, 4128768, "T", "cmr10", 9.96], + [5519588, 4128768, "h", "cmr10", 9.96], + [5883678, 4128768, "i", "cmr10", 9.96], + [6065723, 4128768, "s", "cmr10", 9.96], + [6542679, 4128768, "i", "cmr10", 9.96], + [6724724, 4128768, "s", "cmr10", 9.96], + [7201680, 4128768, "a", "cmr10", 9.96], + [7747814, 4128768, "L", "cmr10", 9.96], + [7921485, 3994421, "A", "cmr7", 6.97], + [8210032, 4128768, "T", "cmr10", 9.96], + [8574098, 4269852, "E", "cmr10", 9.96], + [8938188, 4128768, "X", "cmr10", 9.96], + [9648162, 4128768, "t", "cmr10", 9.96], + [9903025, 4128768, "e", "cmr10", 9.96], + [10194296, 4128768, "s", "cmr10", 9.96], + [10452799, 4128768, "t", "cmr10", 9.96], + [10926115, 4128768, "d", "cmr10", 9.96], + [11290205, 4128768, "o", "cmr10", 9.96], + [11636091, 4128768, "c", "cmr10", 9.96], + [11927362, 4128768, "u", "cmr10", 9.96], + [12291452, 4128768, "m", "cmr10", 9.96], + [12837587, 4128768, "e", "cmr10", 9.96], + [13128858, 4128768, "n", "cmr10", 9.96], + [13474743, 4128768, "t", "cmr10", 9.96], + [4063232, 4915200, "f", "cmr10", 9.96], + [4263482, 4915200, "o", "cmr10", 9.96], + [4591163, 4915200, "r", "cmr10", 9.96], + [5066299, 4915200, "t", "cmr10", 9.96], + [5321162, 4915200, "e", "cmr10", 9.96], + [5612433, 4915200, "s", "cmr10", 9.96], + [5870936, 4915200, "t", "cmr10", 9.96], + [6125799, 4915200, "i", "cmr10", 9.96], + [6307844, 4915200, "n", "cmr10", 9.96], + [6671934, 4915200, "g", "cmr10", 9.96], + [7218068, 4915200, "m", "cmr10", 9.96], + [7764203, 4915200, "a", "cmr10", 9.96], + [8091884, 4915200, "t", "cmr10", 9.96], + [8346747, 4915200, "p", "cmr10", 9.96], + [8710837, 4915200, "l", "cmr10", 9.96], + [8892882, 4915200, "o", "cmr10", 9.96], + [9220563, 4915200, "t", "cmr10", 9.96], + [9475426, 4915200, "l", "cmr10", 9.96], + [9657471, 4915200, "i", "cmr10", 9.96], + [9839516, 4915200, "b", "cmr10", 9.96], + [10203606, 4915200, "'", "cmr10", 9.96], + [10385651, 4915200, "s", "cmr10", 9.96], + [10862607, 4915200, "d", "cmr10", 9.96], + [11226697, 4915200, "v", "cmr10", 9.96], + [11572583, 4915200, "i", "cmr10", 9.96], + [11754628, 4915200, "r", "cmr10", 9.96], + [12011311, 4915200, "e", "cmr10", 9.96], + [12302582, 4915200, "a", "cmr10", 9.96], + [12630263, 4915200, "d", "cmr10", 9.96], + [13686591, 6629148, "\u0019", "cmmi5", 4.98], + [13717140, 6963172, "2", "cmr5", 4.98], + [13355327, 7035991, "Z", "cmex10", 9.96], + [13406754, 8897228, "0", "cmr7", 6.97], + [14010688, 7200560, "\u0010", "cmex10", 9.96], + [14937658, 7484660, "x", "cmmi10", 9.96], + [14480727, 8377560, "s", "cmr10", 9.96], + [14739230, 8377560, "i", "cmr10", 9.96], + [14921275, 8377560, "n", "cmr10", 9.96], + [15394589, 8377560, "x", "cmmi10", 9.96], + [15847788, 7200560, "\u0011", "cmex10", 9.96], + [16239184, 7336365, "2", "cmr7", 6.97], + [16642411, 7928016, "d", "cmr10", 9.96], + [17006501, 7928016, "x", "cmmi10", 9.96] + ], + "boxes": [ + [4063232, 5701632, 65536, 22609920], + [13686591, 6703682, 26213, 284106], + [14480727, 7777282, 26213, 1288418] + ] + }, + { + "text": [ + [5046272, 4128768, "a", "cmr10", 9.96], + [5373953, 4128768, "n", "cmr10", 9.96], + [5738043, 4128768, "o", "cmr10", 9.96], + [6065724, 4128768, "t", "cmr10", 9.96], + [6320587, 4128768, "h", "cmr10", 9.96], + [6684677, 4128768, "e", "cmr10", 9.96], + [6975948, 4128768, "r", "cmr10", 9.96], + [7451084, 4128768, "p", "cmr10", 9.96], + [7815174, 4128768, "a", "cmr10", 9.96], + [8142855, 4128768, "g", "cmr10", 9.96], + [8470536, 4128768, "e", "cmr10", 9.96] + ], + "boxes": [] + } +] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.tex b/lib/matplotlib/tests/baseline_images/dviread/test.tex index 4a2d4720c065..33220fedae3e 100644 --- a/lib/matplotlib/tests/baseline_images/dviread/test.tex +++ b/lib/matplotlib/tests/baseline_images/dviread/test.tex @@ -1,19 +1,17 @@ +% source file for test.dvi \documentclass{article} -\usepackage{iftex} -\iftutex\usepackage{fontspec}\fi % xetex or luatex \pagestyle{empty} - \begin{document} -A \LaTeX { - \iftutex\fontspec{DejaVuSans.ttf}[ - FakeSlant=0.25, FakeStretch=1.25, FakeBold=2.5, Color=0000FF]\fi - doc. -} +This is a \LaTeX\ test document\\ +for testing matplotlib's dviread -\newpage +\noindent\rule{\textwidth}{1pt} \[ \int\limits_0^{\frac{\pi}{2}} \Bigl(\frac{x}{\sin x}\Bigr)^2\,\mathrm{d}x \] \special{Special!} +\newpage +another page + \end{document} diff --git a/lib/matplotlib/tests/baseline_images/dviread/xelatex.json b/lib/matplotlib/tests/baseline_images/dviread/xelatex.json deleted file mode 100644 index 8fb81ddf0c7e..000000000000 --- a/lib/matplotlib/tests/baseline_images/dviread/xelatex.json +++ /dev/null @@ -1 +0,0 @@ -[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8176180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8677380, 4128768, "c", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [9127780, 4128768, ".", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738722, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920912, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index cbe2e9a5264c..3f34a58a765d 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -215,15 +215,14 @@ def test_figureoptions(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) -def test_save_figure_return(tmp_path): +def test_save_figure_return(): fig, ax = plt.subplots() ax.imshow([[1]]) - expected = tmp_path / "foobar.png" prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName" - with mock.patch(prop, return_value=(str(expected), None)): + with mock.patch(prop, return_value=("foobar.png", None)): fname = fig.canvas.manager.toolbar.save_figure() - assert fname == str(expected) - assert expected.exists() + 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 diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 33fe9bb150d2..7b7ff151be18 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -2,8 +2,7 @@ from pathlib import Path import shutil -from matplotlib import cbook, dviread as dr -from matplotlib.testing import subprocess_run_for_testing, _has_tex_package +import matplotlib.dviread as dr import pytest @@ -63,85 +62,16 @@ def test_PsfontsMap(monkeypatch): @pytest.mark.skipif(shutil.which("kpsewhich") is None, reason="kpsewhich is not available") -@pytest.mark.parametrize("engine", ["pdflatex", "xelatex", "lualatex"]) -def test_dviread(tmp_path, engine, monkeypatch): - dirpath = Path(__file__).parent / "baseline_images/dviread" - shutil.copy(dirpath / "test.tex", tmp_path) - shutil.copy(cbook._get_data_path("fonts/ttf/DejaVuSans.ttf"), tmp_path) - cmd, fmt = { - "pdflatex": (["latex"], "dvi"), - "xelatex": (["xelatex", "-no-pdf"], "xdv"), - "lualatex": (["lualatex", "-output-format=dvi"], "dvi"), - }[engine] - if shutil.which(cmd[0]) is None: - pytest.skip(f"{cmd[0]} is not available") - subprocess_run_for_testing( - [*cmd, "test.tex"], cwd=tmp_path, check=True, capture_output=True) - # dviread must be run from the tmppath directory because {xe,lua}tex output - # records the path to DejaVuSans.ttf as it is written in the tex source, - # i.e. as a relative path. - monkeypatch.chdir(tmp_path) - with dr.Dvi(tmp_path / f"test.{fmt}", None) as dvi: - try: - pages = [*dvi] - except FileNotFoundError as exc: - for note in getattr(exc, "__notes__", []): - if "too-old version of luaotfload" in note: - pytest.skip(note) - raise - data = [ - { - "text": [ - [ - t.x, t.y, - t._as_unicode_or_name(), - t.font.resolve_path().name, - round(t.font.size, 2), - t.font.effects, - ] for t in page.text - ], - "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] - } for page in pages - ] - correct = json.loads((dirpath / f"{engine}.json").read_text()) - assert data == correct - - -@pytest.mark.skipif(shutil.which("latex") is None, reason="latex is not available") -@pytest.mark.skipif(not _has_tex_package("concmath"), reason="needs concmath.sty") -def test_dviread_pk(tmp_path): - (tmp_path / "test.tex").write_text(r""" - \documentclass{article} - \usepackage{concmath} - \pagestyle{empty} - \begin{document} - Hi! - \end{document} - """) - subprocess_run_for_testing( - ["latex", "test.tex"], cwd=tmp_path, check=True, capture_output=True) - with dr.Dvi(tmp_path / "test.dvi", None) as dvi: - pages = [*dvi] - data = [ - { - "text": [ - [ - t.x, t.y, - t._as_unicode_or_name(), - t.font.resolve_path().name, - round(t.font.size, 2), - t.font.effects, - ] for t in page.text - ], - "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] - } for page in pages - ] - correct = [{ - 'boxes': [], - 'text': [ - [5046272, 4128768, 'H?', 'ccr10.600pk', 9.96, {}], - [5530510, 4128768, 'i?', 'ccr10.600pk', 9.96, {}], - [5716195, 4128768, '!?', 'ccr10.600pk', 9.96, {}], - ], - }] +def test_dviread(): + dirpath = Path(__file__).parent / 'baseline_images/dviread' + with (dirpath / 'test.json').open() as f: + correct = json.load(f) + with dr.Dvi(str(dirpath / 'test.dvi'), None) as dvi: + data = [{'text': [[t.x, t.y, + chr(t.glyph), + t.font.texname.decode('ascii'), + round(t.font.size, 2)] + for t in page.text], + 'boxes': [[b.x, b.y, b.height, b.width] for b in page.boxes]} + for page in dvi] assert data == correct diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 330a2fab503d..00c223c59362 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1168,6 +1168,21 @@ def test_respects_bbox(): assert buf_before.getvalue() != buf_after.getvalue() # Not all white. +def test_image_cursor_formatting(): + fig, ax = plt.subplots() + # Create a dummy image to be able to call format_cursor_data + im = ax.imshow(np.zeros((4, 4))) + + data = np.ma.masked_array([0], mask=[True]) + assert im.format_cursor_data(data) == '[]' + + data = np.ma.masked_array([0], mask=[False]) + assert im.format_cursor_data(data) == '[0]' + + data = np.nan + assert im.format_cursor_data(data) == '[nan]' + + @check_figures_equal(extensions=['png', 'pdf', 'svg']) def test_image_array_alpha(fig_test, fig_ref): """Per-pixel alpha channel test.""" diff --git a/lib/matplotlib/tests/test_violinplot_datetime.py b/lib/matplotlib/tests/test_violinplot_datetime.py new file mode 100644 index 000000000000..0dadede7752a --- /dev/null +++ b/lib/matplotlib/tests/test_violinplot_datetime.py @@ -0,0 +1,93 @@ +"""Tests for datetime and timedelta support in violinplot.""" + +import datetime +import pytest +import matplotlib.pyplot as plt + + +def make_vpstats(): + """Create minimal valid stats for a violin plot.""" + datetimes = [ + datetime.datetime(2023, 2, 10), + datetime.datetime(2023, 5, 18), + datetime.datetime(2023, 6, 6) + ] + return [{ + 'coords': datetimes, + 'vals': [0.1, 0.5, 0.2], + 'mean': datetimes[1], + 'median': datetimes[1], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + }, { + 'coords': datetimes, + 'vals': [0.2, 0.3, 0.4], + 'mean': datetimes[2], + 'median': datetimes[2], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + }] + + +def test_datetime_positions_with_float_widths_raises(): + """Test that datetime positions with + float widths raise TypeError. + """ + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + + +def test_datetime_positions_with_scalar_float_width_raises(): + """Test that datetime positions with scalar + float width raise TypeError. + """ + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = 0.75 + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + + +def test_numeric_positions_with_float_widths_ok(): + """Test that numeric positions with + float widths work. + """ + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [1.0, 2.0] + widths = [0.5, 1.0] + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + + +def test_mixed_positions_datetime_and_numeric_behaves(): + """Test that mixed datetime and numeric positions with + float widths raise TypeError. + """ + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), 2.0] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 8deae19c42e7..b57597ded363 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -234,9 +234,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # characters into strings. t1_encodings = {} for text in page.text: - font = get_font(text.font.resolve_path()) - if text.font.subfont: - raise NotImplementedError("Indexing TTC fonts is not supported yet") + font = get_font(text.font_path) char_id = self._get_char_id(font, text.glyph) if char_id not in glyph_map: font.clear() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 32da8dfde7aa..3122b64bde73 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -244,8 +244,6 @@ def _transformed_cube(self, vals): (minx, maxy, maxz)] return proj3d._proj_points(xyzs, self.M) - @_api.delete_parameter("3.11", "share") - @_api.delete_parameter("3.11", "anchor") def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ Set the aspect ratios. @@ -265,31 +263,39 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): 'equalyz' adapt the y and z axes to have equal aspect ratios. ========= ================================================== - adjustable : {'box', 'datalim'}, default: 'box' - Defines which parameter to adjust to meet the aspect ratio. - - - 'box': Change the physical dimensions of the axes bounding box. - - 'datalim': Change the x, y, or z data limits. + adjustable : None or {'box', 'datalim'}, optional + If not *None*, this defines which parameter will be adjusted to + meet the required aspect. See `.set_adjustable` for further + details. anchor : None or str or 2-tuple of float, optional - .. deprecated:: 3.11 - This parameter has no effect. + If not *None*, this defines where the Axes will be drawn if there + is extra space due to aspect constraints. The most common way to + specify the anchor are abbreviations of cardinal directions: + + ===== ===================== + value description + ===== ===================== + 'C' centered + 'SW' lower left corner + 'S' middle of bottom edge + 'SE' lower right corner + etc. + ===== ===================== + + See `~.Axes.set_anchor` for further details. share : bool, default: False - .. deprecated:: 3.11 - This parameter has no effect. + If ``True``, apply the settings to all shared Axes. See Also -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ - if adjustable is None: - adjustable = 'box' - _api.check_in_list(['box', 'datalim'], adjustable=adjustable) _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), aspect=aspect) - - self.set_adjustable(adjustable) + super().set_aspect( + aspect='auto', adjustable=adjustable, anchor=anchor, share=share) self._aspect = aspect if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index e38df4f80ba4..370fd506530e 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -17,7 +17,6 @@ from matplotlib.patches import Circle, PathPatch from matplotlib.path import Path from matplotlib.text import Text -from matplotlib import _api import matplotlib.pyplot as plt import numpy as np @@ -2712,31 +2711,3 @@ def test_line3dcollection_autolim_ragged(): assert np.allclose(ax.get_xlim3d(), (-0.08333333333333333, 4.083333333333333)) assert np.allclose(ax.get_ylim3d(), (-0.0625, 3.0625)) assert np.allclose(ax.get_zlim3d(), (-0.08333333333333333, 4.083333333333333)) - - -def test_axes3d_set_aspect_deperecated_params(): - """ - Test that using the deprecated 'anchor' and 'share' kwargs in - set_aspect raises the correct warning. - """ - fig = plt.figure() - ax = fig.add_subplot(projection='3d') - - # Test that providing the `anchor` parameter raises a deprecation warning. - with pytest.warns(_api.MatplotlibDeprecationWarning, match="'anchor' parameter"): - ax.set_aspect('equal', anchor='C') - - # Test that using the 'share' parameter is now deprecated. - with pytest.warns(_api.MatplotlibDeprecationWarning, match="'share' parameter"): - ax.set_aspect('equal', share=True) - - # Test that the `adjustable` parameter is correctly processed to satisfy - # code coverage. - ax.set_aspect('equal', adjustable='box') - assert ax.get_adjustable() == 'box' - - ax.set_aspect('equal', adjustable='datalim') - assert ax.get_adjustable() == 'datalim' - - with pytest.raises(ValueError, match="adjustable"): - ax.set_aspect('equal', adjustable='invalid_value')