diff --git a/.circleci/config.yml b/.circleci/config.yml index bade4c7042a6..713b160661fd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,7 @@ commands: fonts-crosextra-carlito \ fonts-freefont-otf \ fonts-humor-sans \ + fonts-noto-cjk \ optipng fonts-install: diff --git a/.flake8 b/.flake8 index 06ad576c1b19..7094b6c49b5f 100644 --- a/.flake8 +++ b/.flake8 @@ -120,11 +120,15 @@ per-file-ignores = examples/style_sheets/plot_solarizedlight2.py: E501 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 examples/text_labels_and_annotations/custom_legends.py: E402 - examples/ticks_and_spines/date_concise_formatter.py: E402 + examples/ticks/date_concise_formatter.py: E402 examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk3_sgskip.py: E402 - examples/user_interfaces/gtk_spreadsheet_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_sgskip.py: E402 + examples/user_interfaces/gtk3_spreadsheet_sgskip.py: E402 + examples/user_interfaces/gtk4_spreadsheet_sgskip.py: E402 examples/user_interfaces/mpl_with_glade3_sgskip.py: E402 - examples/user_interfaces/pylab_with_gtk_sgskip.py: E402 + examples/user_interfaces/pylab_with_gtk3_sgskip.py: E402 + examples/user_interfaces/pylab_with_gtk4_sgskip.py: E402 examples/user_interfaces/toolmanager_sgskip.py: E402 examples/userdemo/pgf_preamble_sgskip.py: E402 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2bef7ab95a56..5c9afed3c02b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: [numfocus] +github: [matplotlib, numfocus] custom: https://numfocus.org/donate-to-matplotlib diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 9114df686a96..47455e74fac0 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -15,6 +15,7 @@ jobs: env: min-numpy-version: "1.17.3" min-numpy-hash: "b6/d6/be8f975f5322336f62371c9abeb936d592c98c047ad63035f1b38ae08efe" + CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" strategy: matrix: os: [ubuntu-18.04, windows-latest, macos-latest] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b4a1bbbf35fa..c5f578d54be5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,6 +70,7 @@ jobs: cm-super \ dvipng \ ffmpeg \ + fonts-noto-cjk \ gdb \ gir1.2-gtk-3.0 \ graphviz \ @@ -106,6 +107,8 @@ jobs: ;; macOS) brew install ccache + brew tap homebrew/cask-fonts + brew install font-noto-sans-cjk-sc ;; esac diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5d51afa86f67..13e6d709c42f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -82,6 +82,7 @@ stages: cm-super \ dvipng \ ffmpeg \ + fonts-noto-cjk \ gdb \ gir1.2-gtk-3.0 \ graphviz \ @@ -102,6 +103,8 @@ stages: darwin) brew install --cask xquartz brew install pkg-config ffmpeg imagemagick mplayer ccache + brew tap homebrew/cask-fonts + brew install font-noto-sans-cjk-sc ;; win32) ;; diff --git a/doc/_static/mpl.css b/doc/_static/mpl.css index 88c620d559e8..b5ac4b6ddd3a 100644 --- a/doc/_static/mpl.css +++ b/doc/_static/mpl.css @@ -212,3 +212,11 @@ table.property-table th, table.property-table td { padding: 4px 10px; } + +.donate-button { + margin: 1em 0; +} + +.sphinxsidebarwrapper { + margin: 0 1em; +} diff --git a/doc/_templates/cheatsheet_sidebar.html b/doc/_templates/cheatsheet_sidebar.html new file mode 100644 index 000000000000..615c2bc4cd04 --- /dev/null +++ b/doc/_templates/cheatsheet_sidebar.html @@ -0,0 +1,9 @@ + +
+

Matplotlib cheatsheets

+ + Matplotlib cheatsheets + +
diff --git a/doc/_templates/donate_sidebar.html b/doc/_templates/donate_sidebar.html index fc7310b70088..02d5ff6fc46c 100644 --- a/doc/_templates/donate_sidebar.html +++ b/doc/_templates/donate_sidebar.html @@ -1,6 +1,8 @@ - - -
-

Matplotlib cheatsheets

- - Matplotlib cheatsheets - -
diff --git a/doc/api/backend_gtk4_api.rst b/doc/api/backend_gtk4_api.rst new file mode 100644 index 000000000000..c2bc05d6f36c --- /dev/null +++ b/doc/api/backend_gtk4_api.rst @@ -0,0 +1,16 @@ +**NOTE** These backends are not documented here, to avoid adding a dependency +to building the docs. + +.. redirect-from:: /api/backend_gtk4agg_api +.. redirect-from:: /api/backend_gtk4cairo_api + + +:mod:`matplotlib.backends.backend_gtk4agg` +========================================== + +.. module:: matplotlib.backends.backend_gtk4agg + +:mod:`matplotlib.backends.backend_gtk4cairo` +============================================ + +.. module:: matplotlib.backends.backend_gtk4cairo diff --git a/doc/api/index.rst b/doc/api/index.rst index 9c33f38ad6d8..ed0e7f310275 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -101,21 +101,6 @@ Alphabetical list of modules: widgets_api.rst _api_api.rst _enums_api.rst - -Toolkits --------- - -:ref:`toolkits-index` are collections of application-specific functions that extend -Matplotlib. The following toolkits are included: - -.. toctree:: - :hidden: - - toolkits/index.rst - -.. toctree:: - :maxdepth: 1 - toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index ad2febf8dc38..25c06820c9da 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -10,6 +10,7 @@ backend_agg_api.rst backend_cairo_api.rst backend_gtk3_api.rst + backend_gtk4_api.rst backend_nbagg_api.rst backend_pdf_api.rst backend_pgf_api.rst diff --git a/doc/api/next_api_changes/deprecations/20995-AL.rst b/doc/api/next_api_changes/deprecations/20995-AL.rst new file mode 100644 index 000000000000..bdccd8f5d333 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20995-AL.rst @@ -0,0 +1,3 @@ +``backend_gtk3.icon_filename`` and ``backend_gtk3.window_icon`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated with no replacement. diff --git a/doc/api/toolkits/index.rst b/doc/api/toolkits/index.rst deleted file mode 100644 index 59c01ab21a69..000000000000 --- a/doc/api/toolkits/index.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. _toolkits-index: - -.. _toolkits: - -######## -Toolkits -######## - -Toolkits are collections of application-specific functions that extend -Matplotlib. - -.. _toolkit_mplot3d: - -mplot3d -======= - -:mod:`mpl_toolkits.mplot3d` provides some basic 3D -plotting (scatter, surf, line, mesh) tools. Not the fastest or most feature -complete 3D library out there, but it ships with Matplotlib and thus may be a -lighter weight solution for some use cases. Check out the -:doc:`mplot3d tutorial ` for more -information. - -.. figure:: ../../gallery/mplot3d/images/sphx_glr_contourf3d_2_001.png - :target: ../../gallery/mplot3d/contourf3d_2.html - :align: center - :scale: 50 - -.. toctree:: - :maxdepth: 2 - - mplot3d/index.rst - mplot3d/faq.rst - -Links ------ -* mpl3d API: :ref:`toolkit_mplot3d-api` - -.. include:: axes_grid1.rst - :start-line: 1 - -.. include:: axisartist.rst - :start-line: 1 - -.. include:: axes_grid.rst - :start-line: 1 diff --git a/doc/api/toolkits/mplot3d.rst b/doc/api/toolkits/mplot3d.rst index 97d3bf13246f..5b3cb52571bb 100644 --- a/doc/api/toolkits/mplot3d.rst +++ b/doc/api/toolkits/mplot3d.rst @@ -1,8 +1,32 @@ -.. _toolkit_mplot3d-api: +.. _toolkit_mplot3d-index: +.. currentmodule:: mpl_toolkits.mplot3d + +************************ +``mpl_toolkits.mplot3d`` +************************ + +The mplot3d toolkit adds simple 3D plotting capabilities (scatter, surface, +line, mesh, etc.) to Matplotlib by supplying an Axes object that can create +a 2D projection of a 3D scene. The resulting graph will have the same look +and feel as regular 2D plots. Not the fastest or most feature complete 3D +library out there, but it ships with Matplotlib and thus may be a lighter +weight solution for some use cases. + +See the :doc:`mplot3d tutorial ` for +more information. + +.. image:: /_static/demo_mplot3d.png + :align: center + +The interactive backends also provide the ability to rotate and zoom the 3D +scene. One can rotate the 3D scene by simply clicking-and-dragging the scene. +Zooming is done by right-clicking the scene and dragging the mouse up and down +(unlike 2D plots, the toolbar zoom button is not used). + +.. toctree:: + :maxdepth: 2 -*********** -mplot3d API -*********** + mplot3d/faq.rst .. note:: `.pyplot` cannot be used to add content to 3D plots, because its function diff --git a/doc/api/toolkits/mplot3d/index.rst b/doc/api/toolkits/mplot3d/index.rst deleted file mode 100644 index 8b153c06903f..000000000000 --- a/doc/api/toolkits/mplot3d/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _toolkit_mplot3d-index: -.. currentmodule:: mpl_toolkits.mplot3d - -******* -mplot3d -******* - -Matplotlib mplot3d toolkit -========================== -The mplot3d toolkit adds simple 3D plotting capabilities to matplotlib by -supplying an axes object that can create a 2D projection of a 3D scene. -The resulting graph will have the same look and feel as regular 2D plots. - -See the :doc:`mplot3d tutorial ` for -more information on how to use this toolkit. - -.. image:: /_static/demo_mplot3d.png - -The interactive backends also provide the ability to rotate and zoom -the 3D scene. One can rotate the 3D scene by simply clicking-and-dragging -the scene. Zooming is done by right-clicking the scene and dragging the -mouse up and down. Note that one does not use the zoom button like one -would use for regular 2D plots. - -.. toctree:: - :maxdepth: 2 - - faq.rst diff --git a/doc/conf.py b/doc/conf.py index 00f7ed7c5ea2..fc4cdcf09359 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -71,12 +71,15 @@ 'sphinxext.skip_deprecated', 'sphinxext.redirect_from', 'sphinx_copybutton', + 'sphinx_panels', ] exclude_patterns = [ 'api/prev_api_changes/api_changes_*/*', ] +panels_add_bootstrap_css = False + def _check_dependencies(): names = { @@ -291,6 +294,12 @@ def _check_dependencies(): html_theme_options = { "logo_link": "index", "collapse_navigation": True if CIRCLECI else False, + # "external_links": [ + # { + # "name": "Third party packages", + # "url": "https://matplotlib.org/mpl-third-party/" + # }, + # ], "icon_links": [ { "name": "gitter", @@ -312,8 +321,9 @@ def _check_dependencies(): "url": "https://twitter.com/matplotlib/", "icon": "fab fa-twitter-square", }, - ], + "show_prev_next": False, + "navbar_center": ["mpl_nav_bar.html"], } include_analytics = False if include_analytics: @@ -341,8 +351,10 @@ def _check_dependencies(): # Custom sidebar templates, maps page names to templates. html_sidebars = { "index": [ + 'search-field.html', # 'sidebar_announcement.html', "sidebar_versions.html", + "cheatsheet_sidebar.html", "donate_sidebar.html", ], # '**': ['localtoc.html', 'pagesource.html'] diff --git a/doc/contents.rst b/doc/contents.rst index 37fd17172ce2..f9d10936c7fc 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -1,4 +1,4 @@ - +.. _complete_sitemap: Contents ======== @@ -15,13 +15,10 @@ Contents :maxdepth: 2 users/installing.rst - plot_types/index.rst - gallery/index.rst - tutorials/index.rst - api/index.rst users/index.rst + users/backmatter.rst devel/index.rst - Third-party packages + users/release_notes.rst .. only:: html diff --git a/doc/devel/dependencies.rst b/doc/devel/dependencies.rst index 5502664f4b35..1007210b8ba9 100644 --- a/doc/devel/dependencies.rst +++ b/doc/devel/dependencies.rst @@ -42,9 +42,9 @@ and the capabilities they provide. * Tk_ (>= 8.4, != 8.6.0 or 8.6.1) [#]_: for the Tk-based backends. * PyQt6_ (>= 6.1), PySide6_, PyQt5_, or PySide2_: for the Qt-based backends. -* PyGObject_: for the GTK3-based backends [#]_. +* PyGObject_: for the GTK-based backends [#]_. * wxPython_ (>= 4) [#]_: for the wx-based backends. -* pycairo_ (>= 1.11.0) or cairocffi_ (>= 0.8): for the GTK3 and/or cairo-based +* pycairo_ (>= 1.11.0) or cairocffi_ (>= 0.8): for the GTK and/or cairo-based backends. * Tornado_: for the WebAgg backend. diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index aa189948003c..65898b95ee0c 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -244,7 +244,7 @@ The correct target folder can be found using:: python -c "import matplotlib.tests; print(matplotlib.tests.__file__.rsplit('/', 1)[0])" An analogous copying of :file:`lib/mpl_toolkits/tests/baseline_images` -is necessary for testing the :ref:`toolkits`. +is necessary for testing ``mpl_toolkits``. Run the tests ^^^^^^^^^^^^^ diff --git a/doc/faq/index.rst b/doc/faq/index.rst index def68fc84c71..d7840a626d1d 100644 --- a/doc/faq/index.rst +++ b/doc/faq/index.rst @@ -1,8 +1,8 @@ .. _faq-index: -################## -The Matplotlib FAQ -################## +###### +How-To +###### .. only:: html diff --git a/doc/index.rst b/doc/index.rst index 0350a1047a9f..3bfc6313eed4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,43 +4,113 @@ .. module:: matplotlib -Matplotlib documentation ------------------------- -Release: |release| +Matplotlib |release| documentation +---------------------------------- Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. -Learn -===== - -- :doc:`Quick-start Guide ` -- Basic :doc:`Plot Types ` and :doc:`Example Gallery ` -- `Introductory Tutorials <../tutorials/index.html#introductory>`_ -- :doc:`External Learning Resources ` - -Reference -========= +Installation +============ -- :doc:`API Reference ` +.. panels:: + :card: + install-card + :column: col-lg-6 col-md-6 col-sm-12 col-xs-12 p-3 - - :doc:`pyplot API `: top-level interface to create - Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, - `.pyplot.subplot_mosaic`) - - :doc:`Axes API ` for *most* plotting methods - - :doc:`Figure API ` for figure-level methods + Installing using conda + ^^^^^^^^^^^^^^^^^^^ -- :doc:`Extra Toolkits ` + Matplotlib is part of the `Anaconda `__ + distribution and can be installed with Anaconda or Miniconda: -How-tos -======= -- :doc:`Installation Guide ` -- :doc:`Contributing to Matplotlib ` -- :doc:`Matplotlib FAQ ` + .. code-block:: bash -Understand how Matplotlib works -=============================== + conda install matplotlib -- Many of the :doc:`Tutorials ` have explanatory material + --- + + Installing using pip + ^^^^^^^^^^^ + + Matplotlib provides wheels for Linux, OSX, and Windows which can be + installed via pip from `PyPI `__. + + + .. code-block:: bash + + pip install matplotlib + + +Further details are availabe in the :doc:`Installation Guide `. + + +Learning resources +================== + + +.. panels:: + + Tutorials + ^^^^^^^^^ + + - :doc:`Quick-start Guide ` + - Basic :doc:`Plot Types ` + - `Introductory Tutorials <../tutorials/index.html#introductory>`_ + - :doc:`External Learning Resources ` + + --- + + How-tos + ^^^^^^^ + - :doc:`Example Gallery ` + - :doc:`Matplotlib FAQ ` + + --- + + Understand how Matplotlib works + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + - The :ref:`users-guide-explain` section of the :doc:`Users guide ` + - Many of the :doc:`Tutorials ` have explanatory material + + --- + + Reference + ^^^^^^^^^ + + - :doc:`API Reference ` + + - :doc:`Axes API ` for *most* plotting methods + - :doc:`Figure API ` for figure-level methods + - Top-level interface to create Figures (`.pyplot.figure`) and Subplots + (`.pyplot.subplots`, `.pyplot.subplot_mosaic`) + + - :doc:`Extra Toolkits ` + + + +Third-party packages +-------------------- + +There are many `Third-party packages +`_ built on top of and extending +Matplotlib. + + +Contributing +------------ + +Matplotlib is a community project maitained for and by its users. There are many ways +you can help! + +- Help other users `on discourse `__ +- report a bug or request a feature `on GitHub `__ +- or improve the :ref:`documentation and code ` + + +Site Map +-------- + +The :ref:`complete contents of the docs `. diff --git a/doc/thirdpartypackages/index.rst b/doc/thirdpartypackages/index.rst index e6c6aa401526..81dc4d710a52 100644 --- a/doc/thirdpartypackages/index.rst +++ b/doc/thirdpartypackages/index.rst @@ -1,369 +1,5 @@ :orphan: -.. note:: +.. raw:: html - This page has been moved to , - where you will find an up-to-date list of packages. - - -******************** -Third party packages -******************** - -Several external packages that extend or build on Matplotlib functionality are -listed below. You can find more packages at `PyPI `_. -They are maintained and distributed separately from Matplotlib, -and thus need to be installed individually. - -If you have a created a package that extends or builds on Matplotlib -and would like to have your package listed on this page, please submit -an issue or pull request on GitHub. The pull request should include a short -description of the library and an image demonstrating the functionality. -To be included in the PyPI listing, please include ``Framework :: Matplotlib`` -in the classifier list in the ``setup.py`` file for your package. We are also -happy to host third party packages within the `Matplotlib GitHub Organization -`_. - - -Mapping toolkits -**************** - -Basemap -======= -`Basemap `_ plots data on map projections, -with continental and political boundaries. - -.. image:: /_static/basemap_contour1.png - :height: 400px - -Cartopy -======= -`Cartopy `_ builds on top -of Matplotlib to provide object oriented map projection definitions -and close integration with Shapely for powerful yet easy-to-use vector -data processing tools. An example plot from the `Cartopy gallery -`_: - -.. image:: /_static/cartopy_hurricane_katrina_01_00.png - :height: 400px - -Geoplot -======= -`Geoplot `_ builds on top -of Matplotlib and Cartopy to provide a "standard library" of simple, powerful, -and customizable plot types. An example plot from the `Geoplot gallery -`_: - -.. image:: /_static/geoplot_nyc_traffic_tickets.png - :height: 400px - -Ridge Map -========= -`ridge_map `_ uses Matplotlib, -SRTM.py, NumPy, and scikit-image to make ridge plots of your favorite -ridges. - -.. image:: /_static/ridge_map_white_mountains.png - :height: 364px - -Declarative libraries -********************* - -ggplot -====== -`ggplot `_ is a port of the R ggplot2 package -to python based on Matplotlib. - -.. image:: /_static/ggplot.png - :height: 195px - -holoviews -========= -`holoviews `_ makes it easier to visualize data -interactively, especially in a `Jupyter notebook `_, by -providing a set of declarative plotting objects that store your data and -associated metadata. Your data is then immediately visualizable alongside or -overlaid with other data, either statically or with automatically provided -widgets for parameter exploration. - -.. image:: /_static/holoviews.png - :height: 354px - -plotnine -======== - -`plotnine `_ implements a grammar -of graphics, similar to R's `ggplot2 `_. -The grammar allows users to compose plots by explicitly mapping data to the -visual objects that make up the plot. - -.. image:: /_static/plotnine.png - -Specialty plots -*************** - -Broken Axes -=========== -`brokenaxes `_ supplies an axes -class that can have a visual break to indicate a discontinuous range. - -.. image:: /_static/brokenaxes.png - -DeCiDa -====== - -`DeCiDa `_ is a library of functions -and classes for electron device characterization, electronic circuit design and -general data visualization and analysis. - -matplotlib-scalebar -=================== - -`matplotlib-scalebar `_ provides a new artist to display a scale bar, aka micron bar. -It is particularly useful when displaying calibrated images plotted using ``plt.imshow(...)``. - -.. image:: /_static/gold_on_carbon.jpg - -Matplotlib-Venn -=============== -`Matplotlib-Venn `_ provides a -set of functions for plotting 2- and 3-set area-weighted (or unweighted) Venn -diagrams. - -mpl-probscale -============= -`mpl-probscale `_ is a small extension -that allows Matplotlib users to specify probability scales. Simply importing the -``probscale`` module registers the scale with Matplotlib, making it accessible -via e.g., ``ax.set_xscale('prob')`` or ``plt.yscale('prob')``. - -.. image:: /_static/probscale_demo.png - -mpl-scatter-density -=================== - -`mpl-scatter-density `_ is a -small package that makes it easy to make scatter plots of large numbers -of points using a density map. The following example contains around 13 million -points and the plotting (excluding reading in the data) took less than a -second on an average laptop: - -.. image:: /_static/mpl-scatter-density.png - :height: 400px - -When used in interactive mode, the density map is downsampled on-the-fly while -panning/zooming in order to provide a smooth interactive experience. - -mplstereonet -============ -`mplstereonet `_ provides -stereonets for plotting and analyzing orientation data in Matplotlib. - -Natgrid -======= -`mpl_toolkits.natgrid `_ is an interface -to the natgrid C library for gridding irregularly spaced data. - -pyUpSet -======= -`pyUpSet `_ is a -static Python implementation of the `UpSet suite by Lex et al. -`_ to explore complex intersections of -sets and data frames. - -seaborn -======= -`seaborn `_ is a high level interface for drawing -statistical graphics with Matplotlib. It aims to make visualization a central -part of exploring and understanding complex datasets. - -.. image:: /_static/seaborn.png - :height: 157px - -WCSAxes -======= - -The `Astropy `_ core package includes a submodule -called WCSAxes (available at `astropy.visualization.wcsaxes -`_) which -adds Matplotlib projections for Astronomical image data. The following is an -example of a plot made with WCSAxes which includes the original coordinate -system of the image and an overlay of a different coordinate system: - -.. image:: /_static/wcsaxes.jpg - :height: 400px - -Windrose -======== -`Windrose `_ is a Python Matplotlib, -Numpy library to manage wind data, draw windroses (also known as polar rose -plots), draw probability density functions and fit Weibull distributions. - -Yellowbrick -=========== -`Yellowbrick `_ is a suite of visual diagnostic tools for machine learning that enables human steering of the model selection process. Yellowbrick combines scikit-learn with matplotlib using an estimator-based API called the ``Visualizer``, which wraps both sklearn models and matplotlib Axes. ``Visualizer`` objects fit neatly into the machine learning workflow allowing data scientists to integrate visual diagnostic and model interpretation tools into experimentation without extra steps. - -.. image:: /_static/yellowbrick.png - :height: 400px - -Animations -********** - -animatplot -========== -`animatplot `_ is a library for -producing interactive animated plots with the goal of making production of -animated plots almost as easy as static ones. - -.. image:: /_static/animatplot.png - -For an animated version of the above picture and more examples, see the -`animatplot gallery. `_ - -gif -=== -`gif `_ is an ultra lightweight animated gif API. - -.. image:: /_static/gif_attachment_example.png - -numpngw -======= - -`numpngw `_ provides functions for writing -NumPy arrays to PNG and animated PNG files. It also includes the class -``AnimatedPNGWriter`` that can be used to save a Matplotlib animation as an -animated PNG file. See the example on the PyPI page or at the ``numpngw`` -`github repository `_. - -.. image:: /_static/numpngw_animated_example.png - -Interactivity -************* - -mplcursors -========== -`mplcursors `_ provides interactive data -cursors for Matplotlib. - -MplDataCursor -============= -`MplDataCursor `_ is a toolkit -written by Joe Kington to provide interactive "data cursors" (clickable -annotation boxes) for Matplotlib. - -mpl_interactions -================ -`mpl_interactions `_ -makes it easy to create interactive plots controlled by sliders and other -widgets. It also provides several handy capabilities such as manual -image segmentation, comparing cross-sections of arrays, and using the -scroll wheel to zoom. - -.. image:: /_static/mpl-interactions-slider-animated.png - -Rendering backends -****************** - -mplcairo -======== -`mplcairo `_ is a cairo backend for -Matplotlib, with faster and more accurate marker drawing, support for a wider -selection of font formats and complex text layout, and various other features. - -gr -== -`gr `_ is a framework for cross-platform -visualisation applications, which can be used as a high-performance Matplotlib -backend. - -GUI integration -*************** - -wxmplot -======= -`WXMPlot `_ provides advanced wxPython -widgets for plotting and image display of numerical data based on Matplotlib. - -Miscellaneous -************* - -adjustText -========== -`adjustText `_ is a small library for -automatically adjusting text position in Matplotlib plots to minimize overlaps -between them, specified points and other objects. - -.. image:: /_static/adjustText.png - -iTerm2 terminal backend -======================= -`matplotlib_iterm2 `_ is an -external Matplotlib backend using the iTerm2 nightly build inline image display -feature. - -.. image:: /_static/matplotlib_iterm2_demo.png - -mpl-template -============ -`mpl-template `_ provides -a customizable way to add engineering figure elements such as a title block, -border, and logo. - -.. image:: /_static/mpl_template_example.png - :height: 330px - -figpager -======== -`figpager `_ provides customizable figure -elements such as text, lines and images and subplot layout control for single -or multi page output. - - .. image:: /_static/figpager.png - -blume -===== - -`blume `_ provides a replacement for -the Matplotlib ``table`` module. It fixes a number of issues with the -existing table. See the `blume github repository -`_ for more details. - -.. image:: /_static/blume_table_example.png - -highlight-text -============== - -`highlight-text `_ is a small library -that provides an easy way to effectively annotate plots by highlighting -substrings with the font properties of your choice. -See the `highlight-text github repository -`_ for more details and examples. - -.. image:: /_static/highlight_text_examples.png - -DNA Features Viewer -=================== - -`DNA Features Viewer `_ -provides methods to plot annotated DNA sequence maps (possibly along other Matplotlib -plots) for Bioinformatics and Synthetic Biology applications. - -.. image:: /_static/dna_features_viewer_screenshot.png - -GUI applications -**************** - -sviewgui -======== - -`sviewgui `_ is a PyQt-based GUI for -visualisation of data from csv files or `pandas.DataFrame`\s. Main features: - -- Scatter, line, density, histogram, and box plot types -- Settings for the marker size, line width, number of bins of histogram, - colormap (from cmocean) -- Save figure as editable PDF -- Code of the plotted graph is available so that it can be reused and modified - outside of sviewgui - -.. image:: /_static/sviewgui_sample.png + diff --git a/doc/users/backmatter.rst b/doc/users/backmatter.rst new file mode 100644 index 000000000000..8d9fc0407d6f --- /dev/null +++ b/doc/users/backmatter.rst @@ -0,0 +1,11 @@ + +Project Information +------------------- + +.. toctree:: + :maxdepth: 1 + + license.rst + ../citing.rst + credits.rst + history.rst diff --git a/doc/users/explain.rst b/doc/users/explain.rst new file mode 100644 index 000000000000..d9d7a8474dbb --- /dev/null +++ b/doc/users/explain.rst @@ -0,0 +1,10 @@ +.. _users-guide-explain: + +Explanations +------------ + +.. toctree:: + :maxdepth: 1 + + interactive.rst + fonts.rst diff --git a/doc/users/fonts.rst b/doc/users/fonts.rst index e385c98284c0..1b9861df7414 100644 --- a/doc/users/fonts.rst +++ b/doc/users/fonts.rst @@ -1,16 +1,16 @@ -Fonts in Matplotlib Text Engine +Fonts in Matplotlib text engine =============================== Matplotlib needs fonts to work with its text engine, some of which are shipped alongside the installation. However, users can configure the default fonts, or -even provide their own custom fonts! For more details, see :doc:`Customizing +even provide their own custom fonts! For more details, see :doc:`Customizing text properties `. However, Matplotlib also provides an option to offload text rendering to a TeX engine (``usetex=True``), see :doc:`Text rendering with LaTeX `. -Font Specifications +Font specifications ------------------- Fonts have a long and sometimes incompatible history in computing, leading to different platforms supporting different types of fonts. In practice, there are @@ -40,7 +40,7 @@ fonts', more about which is explained later in the guide): NOTE: Adobe will disable support for authoring with Type 1 fonts in January 2023. `Read more here. `_ -Special Mentions +Special mentions ^^^^^^^^^^^^^^^^ Other font specifications which Matplotlib supports: diff --git a/doc/users/index.rst b/doc/users/index.rst index c4ec864df787..32d70d3537fe 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -1,23 +1,24 @@ .. _users-guide-index: -############ -User's guide -############ +########### +Usage Guide +########### .. only:: html :Release: |version| :Date: |today| + + + .. toctree:: - :maxdepth: 2 - - interactive.rst - fonts.rst - release_notes.rst - license.rst - ../citing.rst - ../resources/index.rst - ../faq/index.rst - credits.rst - history.rst + :maxdepth: 2 + + ../plot_types/index.rst + ../tutorials/index.rst + ../gallery/index.rst + explain.rst + ../faq/index.rst + ../api/index.rst + ../resources/index.rst diff --git a/doc/users/next_whats_new/callbacks_on_norms.rst b/doc/users/next_whats_new/callbacks_on_norms.rst new file mode 100644 index 000000000000..1904a92d2fba --- /dev/null +++ b/doc/users/next_whats_new/callbacks_on_norms.rst @@ -0,0 +1,8 @@ +A callback registry has been added to Normalize objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.colors.Normalize` objects now have a callback registry, ``callbacks``, +that can be connected to by other objects to be notified when the norm is +updated. The callback emits the key ``changed`` when the norm is modified. +`.cm.ScalarMappable` is now a listener and will register a change +when the norm's vmin, vmax or other attributes are changed. diff --git a/doc/users/next_whats_new/fig_draw_no_output.rst b/doc/users/next_whats_new/fig_draw_no_output.rst deleted file mode 100644 index 293c6590b8c9..000000000000 --- a/doc/users/next_whats_new/fig_draw_no_output.rst +++ /dev/null @@ -1,10 +0,0 @@ -Figure now has draw_no_output method ------------------------------------- - -Rarely, the user will want to trigger a draw without making output to -either the screen or a file. This is useful for determining the final -position of artists on the figure that require a draw, like text artists. -This could be accomplished via ``fig.canvas.draw()`` but has side effects, -sometimes requires an open file, and is documented on an object most users -do not need to access. The `.Figure.draw_no_output` is provided to trigger -a draw without pushing to the final output, and with fewer side effects. \ No newline at end of file diff --git a/doc/users/next_whats_new/fig_draw_without_rendering.rst b/doc/users/next_whats_new/fig_draw_without_rendering.rst new file mode 100644 index 000000000000..8b9e3147bf07 --- /dev/null +++ b/doc/users/next_whats_new/fig_draw_without_rendering.rst @@ -0,0 +1,12 @@ +Figure now has ``draw_without_rendering`` method +------------------------------------------------ + +Some aspects of a figure are only determined at draw-time, such as the exact +position of text artists or deferred computation like automatic data limits. +If you need these values, you can use ``figure.canvas.draw()`` to force a full +draw. However, this has side effects, sometimes requires an open file, and is +doing more work than is needed. + +The new `.Figure.draw_without_rendering` method runs all the updates that +``draw()`` does, but skips rendering the figure. It's thus more efficient if you +need the updated values to configure further aspects of the figure. diff --git a/doc/users/prev_whats_new/whats_new_0.99.rst b/doc/users/prev_whats_new/whats_new_0.99.rst index 94abde7fe4da..c2d761a25031 100644 --- a/doc/users/prev_whats_new/whats_new_0.99.rst +++ b/doc/users/prev_whats_new/whats_new_0.99.rst @@ -112,7 +112,7 @@ that denote the data limits -- in various arbitrary locations. No longer are your axis lines constrained to be a simple rectangle around the figure -- you can turn on or off left, bottom, right and top, as well as "detach" the spine to offset it away from the data. See -:doc:`/gallery/ticks_and_spines/spine_placement_demo` and +:doc:`/gallery/spines/spine_placement_demo` and :class:`matplotlib.spines.Spine`. .. plot:: diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index d8458e5ba1c2..af06f437eda2 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -6,188 +6,9 @@ Release notes ============= -.. include from another document so that it's easy to exclude this for releases -.. include:: release_notes_next.rst - -Version 3.4 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.4.0.rst - ../api/prev_api_changes/api_changes_3.4.2.rst - ../api/prev_api_changes/api_changes_3.4.0.rst - prev_whats_new/github_stats_3.4.1.rst - prev_whats_new/github_stats_3.4.0.rst - -Version 3.3 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.3.0.rst - ../api/prev_api_changes/api_changes_3.3.1.rst - ../api/prev_api_changes/api_changes_3.3.0.rst - prev_whats_new/github_stats_3.3.4.rst - prev_whats_new/github_stats_3.3.3.rst - prev_whats_new/github_stats_3.3.2.rst - prev_whats_new/github_stats_3.3.1.rst - prev_whats_new/github_stats_3.3.0.rst - -Version 3.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.2.0.rst - ../api/prev_api_changes/api_changes_3.2.0.rst - prev_whats_new/github_stats_3.2.2.rst - prev_whats_new/github_stats_3.2.1.rst - prev_whats_new/github_stats_3.2.0.rst - -Version 3.1 -=========== .. toctree:: :maxdepth: 1 + :glob: + :reversed: - prev_whats_new/whats_new_3.1.0.rst - ../api/prev_api_changes/api_changes_3.1.1.rst - ../api/prev_api_changes/api_changes_3.1.0.rst - prev_whats_new/github_stats_3.1.3.rst - prev_whats_new/github_stats_3.1.2.rst - prev_whats_new/github_stats_3.1.1.rst - prev_whats_new/github_stats_3.1.0.rst - -Version 3.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.0.rst - ../api/prev_api_changes/api_changes_3.0.1.rst - ../api/prev_api_changes/api_changes_3.0.0.rst - prev_whats_new/github_stats_3.0.3.rst - prev_whats_new/github_stats_3.0.2.rst - prev_whats_new/github_stats_3.0.1.rst - prev_whats_new/github_stats_3.0.0.rst - -Version 2.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.2.rst - ../api/prev_api_changes/api_changes_2.2.0.rst - -Version 2.1 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.1.0.rst - ../api/prev_api_changes/api_changes_2.1.2.rst - ../api/prev_api_changes/api_changes_2.1.1.rst - ../api/prev_api_changes/api_changes_2.1.0.rst - -Version 2.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.0.0.rst - ../api/prev_api_changes/api_changes_2.0.1.rst - ../api/prev_api_changes/api_changes_2.0.0.rst - -Version 1.5 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.5.rst - ../api/prev_api_changes/api_changes_1.5.3.rst - ../api/prev_api_changes/api_changes_1.5.2.rst - ../api/prev_api_changes/api_changes_1.5.0.rst - -Version 1.4 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.4.rst - ../api/prev_api_changes/api_changes_1.4.x.rst - -Version 1.3 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.3.rst - ../api/prev_api_changes/api_changes_1.3.x.rst - -Version 1.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.2.2.rst - prev_whats_new/whats_new_1.2.rst - ../api/prev_api_changes/api_changes_1.2.x.rst - -Version 1.1 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.1.rst - ../api/prev_api_changes/api_changes_1.1.x.rst - -Version 1.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.0.rst - -Version 0.x -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/changelog.rst - prev_whats_new/whats_new_0.99.rst - ../api/prev_api_changes/api_changes_0.99.x.rst - ../api/prev_api_changes/api_changes_0.99.rst - prev_whats_new/whats_new_0.98.4.rst - ../api/prev_api_changes/api_changes_0.98.x.rst - ../api/prev_api_changes/api_changes_0.98.1.rst - ../api/prev_api_changes/api_changes_0.98.0.rst - ../api/prev_api_changes/api_changes_0.91.2.rst - ../api/prev_api_changes/api_changes_0.91.0.rst - ../api/prev_api_changes/api_changes_0.90.1.rst - ../api/prev_api_changes/api_changes_0.90.0.rst - - ../api/prev_api_changes/api_changes_0.87.7.rst - ../api/prev_api_changes/api_changes_0.86.rst - ../api/prev_api_changes/api_changes_0.85.rst - ../api/prev_api_changes/api_changes_0.84.rst - ../api/prev_api_changes/api_changes_0.83.rst - ../api/prev_api_changes/api_changes_0.82.rst - ../api/prev_api_changes/api_changes_0.81.rst - ../api/prev_api_changes/api_changes_0.80.rst - - ../api/prev_api_changes/api_changes_0.73.rst - ../api/prev_api_changes/api_changes_0.72.rst - ../api/prev_api_changes/api_changes_0.71.rst - ../api/prev_api_changes/api_changes_0.70.rst - - ../api/prev_api_changes/api_changes_0.65.1.rst - ../api/prev_api_changes/api_changes_0.65.rst - ../api/prev_api_changes/api_changes_0.63.rst - ../api/prev_api_changes/api_changes_0.61.rst - ../api/prev_api_changes/api_changes_0.60.rst - - ../api/prev_api_changes/api_changes_0.54.3.rst - ../api/prev_api_changes/api_changes_0.54.rst - ../api/prev_api_changes/api_changes_0.50.rst - ../api/prev_api_changes/api_changes_0.42.rst - ../api/prev_api_changes/api_changes_0.40.rst + relnotes/* diff --git a/doc/users/relnotes/0.x.rst b/doc/users/relnotes/0.x.rst new file mode 100644 index 000000000000..baddb784b6f2 --- /dev/null +++ b/doc/users/relnotes/0.x.rst @@ -0,0 +1,40 @@ +Version 0.x +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/changelog.rst + ../prev_whats_new/whats_new_0.99.rst + ../../api/prev_api_changes/api_changes_0.99.x.rst + ../../api/prev_api_changes/api_changes_0.99.rst + ../prev_whats_new/whats_new_0.98.4.rst + ../../api/prev_api_changes/api_changes_0.98.x.rst + ../../api/prev_api_changes/api_changes_0.98.1.rst + ../../api/prev_api_changes/api_changes_0.98.0.rst + ../../api/prev_api_changes/api_changes_0.91.2.rst + ../../api/prev_api_changes/api_changes_0.91.0.rst + ../../api/prev_api_changes/api_changes_0.90.1.rst + ../../api/prev_api_changes/api_changes_0.90.0.rst + ../../api/prev_api_changes/api_changes_0.87.7.rst + ../../api/prev_api_changes/api_changes_0.86.rst + ../../api/prev_api_changes/api_changes_0.85.rst + ../../api/prev_api_changes/api_changes_0.84.rst + ../../api/prev_api_changes/api_changes_0.83.rst + ../../api/prev_api_changes/api_changes_0.82.rst + ../../api/prev_api_changes/api_changes_0.81.rst + ../../api/prev_api_changes/api_changes_0.80.rst + ../../api/prev_api_changes/api_changes_0.73.rst + ../../api/prev_api_changes/api_changes_0.72.rst + ../../api/prev_api_changes/api_changes_0.71.rst + ../../api/prev_api_changes/api_changes_0.70.rst + ../../api/prev_api_changes/api_changes_0.65.1.rst + ../../api/prev_api_changes/api_changes_0.65.rst + ../../api/prev_api_changes/api_changes_0.63.rst + ../../api/prev_api_changes/api_changes_0.61.rst + ../../api/prev_api_changes/api_changes_0.60.rst + ../../api/prev_api_changes/api_changes_0.54.3.rst + ../../api/prev_api_changes/api_changes_0.54.rst + ../../api/prev_api_changes/api_changes_0.50.rst + ../../api/prev_api_changes/api_changes_0.42.rst + ../../api/prev_api_changes/api_changes_0.40.rst diff --git a/doc/users/relnotes/1.x.rst b/doc/users/relnotes/1.x.rst new file mode 100644 index 000000000000..fe4e273c6917 --- /dev/null +++ b/doc/users/relnotes/1.x.rst @@ -0,0 +1,20 @@ +Version 1.x +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_1.5.rst + ../../api/prev_api_changes/api_changes_1.5.3.rst + ../../api/prev_api_changes/api_changes_1.5.2.rst + ../prev_whats_new/whats_new_1.4.rst + ../../api/prev_api_changes/api_changes_1.4.x.rst + ../../api/prev_api_changes/api_changes_1.5.0.rst + ../prev_whats_new/whats_new_1.3.rst + ../../api/prev_api_changes/api_changes_1.3.x.rst + ../prev_whats_new/whats_new_1.2.2.rst + ../prev_whats_new/whats_new_1.2.rst + ../../api/prev_api_changes/api_changes_1.2.x.rst + ../prev_whats_new/whats_new_1.1.rst + ../../api/prev_api_changes/api_changes_1.1.x.rst + ../prev_whats_new/whats_new_1.0.rst diff --git a/doc/users/relnotes/2.0.rst b/doc/users/relnotes/2.0.rst new file mode 100644 index 000000000000..892c8af988f8 --- /dev/null +++ b/doc/users/relnotes/2.0.rst @@ -0,0 +1,9 @@ +Version 2.0 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.0.0.rst + ../../api/prev_api_changes/api_changes_2.0.1.rst + ../../api/prev_api_changes/api_changes_2.0.0.rst diff --git a/doc/users/relnotes/2.1.rst b/doc/users/relnotes/2.1.rst new file mode 100644 index 000000000000..262c1b9bfc4f --- /dev/null +++ b/doc/users/relnotes/2.1.rst @@ -0,0 +1,10 @@ +Version 2.1 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.1.0.rst + ../../api/prev_api_changes/api_changes_2.1.2.rst + ../../api/prev_api_changes/api_changes_2.1.1.rst + ../../api/prev_api_changes/api_changes_2.1.0.rst diff --git a/doc/users/relnotes/2.2.rst b/doc/users/relnotes/2.2.rst new file mode 100644 index 000000000000..4c23465627ff --- /dev/null +++ b/doc/users/relnotes/2.2.rst @@ -0,0 +1,8 @@ +Version 2.2 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.2.rst + ../../api/prev_api_changes/api_changes_2.2.0.rst diff --git a/doc/users/relnotes/3.0.rst b/doc/users/relnotes/3.0.rst new file mode 100644 index 000000000000..1f8f4bb4679b --- /dev/null +++ b/doc/users/relnotes/3.0.rst @@ -0,0 +1,10 @@ +Version 3.0 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.0.rst + ../../api/prev_api_changes/api_changes_3.0.1.rst + ../../api/prev_api_changes/api_changes_3.0.0.rst + ../prev_whats_new/github_stats_3.0.2.rst diff --git a/doc/users/relnotes/3.1.rst b/doc/users/relnotes/3.1.rst new file mode 100644 index 000000000000..3d4c88209e12 --- /dev/null +++ b/doc/users/relnotes/3.1.rst @@ -0,0 +1,12 @@ +Version 3.1 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.1.0.rst + ../../api/prev_api_changes/api_changes_3.1.1.rst + ../../api/prev_api_changes/api_changes_3.1.0.rst + ../prev_whats_new/github_stats_3.1.2.rst + ../prev_whats_new/github_stats_3.1.1.rst + ../prev_whats_new/github_stats_3.1.0.rst diff --git a/doc/users/relnotes/3.2.rst b/doc/users/relnotes/3.2.rst new file mode 100644 index 000000000000..c7512026ea87 --- /dev/null +++ b/doc/users/relnotes/3.2.rst @@ -0,0 +1,11 @@ +Version 3.2 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.2.0.rst + ../../api/prev_api_changes/api_changes_3.2.0.rst + ../prev_whats_new/github_stats_3.2.2.rst + ../prev_whats_new/github_stats_3.2.1.rst + ../prev_whats_new/github_stats_3.2.0.rst diff --git a/doc/users/relnotes/3.3.rst b/doc/users/relnotes/3.3.rst new file mode 100644 index 000000000000..f45253265379 --- /dev/null +++ b/doc/users/relnotes/3.3.rst @@ -0,0 +1,14 @@ +Version 3.3 +=========== + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.3.0.rst + ../../api/prev_api_changes/api_changes_3.3.1.rst + ../../api/prev_api_changes/api_changes_3.3.0.rst + ../prev_whats_new/github_stats_3.3.4.rst + ../prev_whats_new/github_stats_3.3.3.rst + ../prev_whats_new/github_stats_3.3.2.rst + ../prev_whats_new/github_stats_3.3.1.rst + ../prev_whats_new/github_stats_3.3.0.rst diff --git a/doc/users/relnotes/3.4.rst b/doc/users/relnotes/3.4.rst new file mode 100644 index 000000000000..4d7562be18cd --- /dev/null +++ b/doc/users/relnotes/3.4.rst @@ -0,0 +1,11 @@ +Version 3.4 +=========== + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.4.0.rst + ../../api/prev_api_changes/api_changes_3.4.2.rst + ../../api/prev_api_changes/api_changes_3.4.0.rst + ../prev_whats_new/github_stats_3.4.1.rst + ../prev_whats_new/github_stats_3.4.0.rst diff --git a/doc/users/relnotes/3.5.rst b/doc/users/relnotes/3.5.rst new file mode 100644 index 000000000000..44f49b923d49 --- /dev/null +++ b/doc/users/relnotes/3.5.rst @@ -0,0 +1,9 @@ +Version 3.5 +=========== + +.. toctree:: + :maxdepth: 1 + + ../next_whats_new + ../../api/next_api_changes + ../github_stats diff --git a/examples/axisartist/demo_axisline_style.py b/examples/axisartist/demo_axisline_style.py index 1427a90952a1..c7270941dadf 100644 --- a/examples/axisartist/demo_axisline_style.py +++ b/examples/axisartist/demo_axisline_style.py @@ -7,8 +7,8 @@ Note: The `mpl_toolkits.axisartist` axes classes may be confusing for new users. If the only aim is to obtain arrow heads at the ends of the axes, -rather check out the -:doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +rather check out the :doc:`/gallery/spines/centered_spines_with_arrows` +example. """ from mpl_toolkits.axisartist.axislines import AxesZero diff --git a/examples/axisartist/demo_parasite_axes.py b/examples/axisartist/demo_parasite_axes.py index ef7d5ca5268d..0b7858f645f3 100644 --- a/examples/axisartist/demo_parasite_axes.py +++ b/examples/axisartist/demo_parasite_axes.py @@ -10,7 +10,7 @@ `mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes`. An alternative approach using standard Matplotlib subplots is shown in the -:doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. +:doc:`/gallery/spines/multiple_yaxis_with_spines` example. An alternative approach using :mod:`mpl_toolkits.axes_grid1` and :mod:`mpl_toolkits.axisartist` is found in the diff --git a/examples/axisartist/demo_parasite_axes2.py b/examples/axisartist/demo_parasite_axes2.py index 3c23e37b7ae7..651cdd032ae5 100644 --- a/examples/axisartist/demo_parasite_axes2.py +++ b/examples/axisartist/demo_parasite_axes2.py @@ -19,7 +19,7 @@ `~.mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes` is the :doc:`/gallery/axisartist/demo_parasite_axes` example. An alternative approach using the usual Matplotlib subplots is shown in -the :doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. +the :doc:`/gallery/spines/multiple_yaxis_with_spines` example. """ from mpl_toolkits.axes_grid1 import host_subplot diff --git a/examples/axisartist/simple_axisartist1.py b/examples/axisartist/simple_axisartist1.py index dacbfd0721f8..95d710cbe0b1 100644 --- a/examples/axisartist/simple_axisartist1.py +++ b/examples/axisartist/simple_axisartist1.py @@ -6,9 +6,9 @@ This example showcases the use of :mod:`.axisartist` to draw spines at custom positions (here, at ``y = 0``). -Note, however, that it is simpler to achieve this effect -using standard `.Spine` methods, as demonstrated in -:doc:`/gallery/ticks_and_spines/centered_spines_with_arrows`. +Note, however, that it is simpler to achieve this effect using standard +`.Spine` methods, as demonstrated in +:doc:`/gallery/spines/centered_spines_with_arrows`. .. redirect-from:: /gallery/axisartist/simple_axisline2 """ diff --git a/examples/spines/README.txt b/examples/spines/README.txt new file mode 100644 index 000000000000..40bc3952eacd --- /dev/null +++ b/examples/spines/README.txt @@ -0,0 +1,4 @@ +.. _spines_examples: + +Spines +====== diff --git a/examples/ticks_and_spines/centered_spines_with_arrows.py b/examples/spines/centered_spines_with_arrows.py similarity index 100% rename from examples/ticks_and_spines/centered_spines_with_arrows.py rename to examples/spines/centered_spines_with_arrows.py diff --git a/examples/ticks_and_spines/multiple_yaxis_with_spines.py b/examples/spines/multiple_yaxis_with_spines.py similarity index 100% rename from examples/ticks_and_spines/multiple_yaxis_with_spines.py rename to examples/spines/multiple_yaxis_with_spines.py diff --git a/examples/ticks_and_spines/spine_placement_demo.py b/examples/spines/spine_placement_demo.py similarity index 97% rename from examples/ticks_and_spines/spine_placement_demo.py rename to examples/spines/spine_placement_demo.py index e567d8160d9c..d433236657f9 100644 --- a/examples/ticks_and_spines/spine_placement_demo.py +++ b/examples/spines/spine_placement_demo.py @@ -6,7 +6,7 @@ Adjusting the location and appearance of axis spines. Note: If you want to obtain arrow heads at the ends of the axes, also check -out the :doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +out the :doc:`/gallery/spines/centered_spines_with_arrows` example. """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/ticks_and_spines/spines.py b/examples/spines/spines.py similarity index 100% rename from examples/ticks_and_spines/spines.py rename to examples/spines/spines.py diff --git a/examples/ticks_and_spines/spines_bounds.py b/examples/spines/spines_bounds.py similarity index 100% rename from examples/ticks_and_spines/spines_bounds.py rename to examples/spines/spines_bounds.py diff --git a/examples/ticks_and_spines/spines_dropped.py b/examples/spines/spines_dropped.py similarity index 100% rename from examples/ticks_and_spines/spines_dropped.py rename to examples/spines/spines_dropped.py diff --git a/examples/text_labels_and_annotations/date.py b/examples/text_labels_and_annotations/date.py index c37a5fb61e31..f1701ad9bc3b 100644 --- a/examples/text_labels_and_annotations/date.py +++ b/examples/text_labels_and_annotations/date.py @@ -16,11 +16,10 @@ An alternative formatter is the `~.dates.ConciseDateFormatter`, used in the second ``Axes`` below (see -:doc:`/gallery/ticks_and_spines/date_concise_formatter`), which often -removes the need to rotate the tick labels. The last ``Axes`` -formats the dates manually, using `~.dates.DateFormatter` to -format the dates using the format strings documented at -`datetime.date.strftime`. +:doc:`/gallery/ticks/date_concise_formatter`), which often removes the need to +rotate the tick labels. The last ``Axes`` formats the dates manually, using +`~.dates.DateFormatter` to format the dates using the format strings documented +at `datetime.date.strftime`. """ import matplotlib.pyplot as plt diff --git a/examples/ticks/README.txt b/examples/ticks/README.txt new file mode 100644 index 000000000000..82441a0d9ee7 --- /dev/null +++ b/examples/ticks/README.txt @@ -0,0 +1,4 @@ +.. _ticks_examples: + +Ticks +===== diff --git a/examples/ticks_and_spines/auto_ticks.py b/examples/ticks/auto_ticks.py similarity index 100% rename from examples/ticks_and_spines/auto_ticks.py rename to examples/ticks/auto_ticks.py diff --git a/examples/ticks_and_spines/centered_ticklabels.py b/examples/ticks/centered_ticklabels.py similarity index 100% rename from examples/ticks_and_spines/centered_ticklabels.py rename to examples/ticks/centered_ticklabels.py diff --git a/examples/ticks_and_spines/colorbar_tick_labelling_demo.py b/examples/ticks/colorbar_tick_labelling_demo.py similarity index 100% rename from examples/ticks_and_spines/colorbar_tick_labelling_demo.py rename to examples/ticks/colorbar_tick_labelling_demo.py diff --git a/examples/ticks_and_spines/custom_ticker1.py b/examples/ticks/custom_ticker1.py similarity index 56% rename from examples/ticks_and_spines/custom_ticker1.py rename to examples/ticks/custom_ticker1.py index ee088fa4d5af..f3e0ec4ef3bf 100644 --- a/examples/ticks_and_spines/custom_ticker1.py +++ b/examples/ticks/custom_ticker1.py @@ -1,28 +1,27 @@ """ -============== -Custom Ticker1 -============== +============= +Custom Ticker +============= -The new ticker code was designed to explicitly support user customized -ticking. The documentation of :mod:`matplotlib.ticker` details this -process. That code defines a lot of preset tickers but was primarily -designed to be user extensible. +The :mod:`matplotlib.ticker` module defines many preset tickers, but was +primarily designed for extensibility, i.e., to support user customized ticking. -In this example a user defined function is used to format the ticks in +In this example, a user defined function is used to format the ticks in millions of dollars on the y axis. """ -import matplotlib.pyplot as plt -money = [1.5e5, 2.5e6, 5.5e6, 2.0e7] +import matplotlib.pyplot as plt def millions(x, pos): """The two arguments are the value and tick position.""" return '${:1.1f}M'.format(x*1e-6) + fig, ax = plt.subplots() -# Use automatic FuncFormatter creation +# set_major_formatter internally creates a FuncFormatter from the callable. ax.yaxis.set_major_formatter(millions) +money = [1.5e5, 2.5e6, 5.5e6, 2.0e7] ax.bar(['Bill', 'Fred', 'Mary', 'Sue'], money) plt.show() @@ -33,6 +32,4 @@ def millions(x, pos): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.pyplot.subplots` # - `matplotlib.axis.Axis.set_major_formatter` -# - `matplotlib.ticker.FuncFormatter` diff --git a/examples/ticks_and_spines/date_concise_formatter.py b/examples/ticks/date_concise_formatter.py similarity index 100% rename from examples/ticks_and_spines/date_concise_formatter.py rename to examples/ticks/date_concise_formatter.py diff --git a/examples/ticks_and_spines/date_demo_convert.py b/examples/ticks/date_demo_convert.py similarity index 100% rename from examples/ticks_and_spines/date_demo_convert.py rename to examples/ticks/date_demo_convert.py diff --git a/examples/ticks_and_spines/date_demo_rrule.py b/examples/ticks/date_demo_rrule.py similarity index 100% rename from examples/ticks_and_spines/date_demo_rrule.py rename to examples/ticks/date_demo_rrule.py diff --git a/examples/ticks_and_spines/date_index_formatter2.py b/examples/ticks/date_index_formatter2.py similarity index 100% rename from examples/ticks_and_spines/date_index_formatter2.py rename to examples/ticks/date_index_formatter2.py diff --git a/examples/ticks_and_spines/date_precision_and_epochs.py b/examples/ticks/date_precision_and_epochs.py similarity index 100% rename from examples/ticks_and_spines/date_precision_and_epochs.py rename to examples/ticks/date_precision_and_epochs.py diff --git a/examples/ticks_and_spines/major_minor_demo.py b/examples/ticks/major_minor_demo.py similarity index 100% rename from examples/ticks_and_spines/major_minor_demo.py rename to examples/ticks/major_minor_demo.py diff --git a/examples/ticks_and_spines/scalarformatter.py b/examples/ticks/scalarformatter.py similarity index 100% rename from examples/ticks_and_spines/scalarformatter.py rename to examples/ticks/scalarformatter.py diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks/tick-formatters.py similarity index 100% rename from examples/ticks_and_spines/tick-formatters.py rename to examples/ticks/tick-formatters.py diff --git a/examples/ticks_and_spines/tick-locators.py b/examples/ticks/tick-locators.py similarity index 100% rename from examples/ticks_and_spines/tick-locators.py rename to examples/ticks/tick-locators.py diff --git a/examples/ticks_and_spines/tick_label_right.py b/examples/ticks/tick_label_right.py similarity index 100% rename from examples/ticks_and_spines/tick_label_right.py rename to examples/ticks/tick_label_right.py diff --git a/examples/ticks_and_spines/tick_labels_from_values.py b/examples/ticks/tick_labels_from_values.py similarity index 100% rename from examples/ticks_and_spines/tick_labels_from_values.py rename to examples/ticks/tick_labels_from_values.py diff --git a/examples/ticks_and_spines/tick_xlabel_top.py b/examples/ticks/tick_xlabel_top.py similarity index 100% rename from examples/ticks_and_spines/tick_xlabel_top.py rename to examples/ticks/tick_xlabel_top.py diff --git a/examples/ticks_and_spines/ticklabels_rotation.py b/examples/ticks/ticklabels_rotation.py similarity index 100% rename from examples/ticks_and_spines/ticklabels_rotation.py rename to examples/ticks/ticklabels_rotation.py diff --git a/examples/ticks_and_spines/README.txt b/examples/ticks_and_spines/README.txt deleted file mode 100644 index e7869c5a08d1..000000000000 --- a/examples/ticks_and_spines/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -.. _ticks_and_spines_examples: - -Ticks and spines -================ diff --git a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py index 2f0833f09511..95d8df21a3a2 100644 --- a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -20,7 +20,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_sgskip.py index f5872304964d..b672ba8d9ff0 100644 --- a/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -19,7 +19,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py new file mode 100644 index 000000000000..685a278fc7ad --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -0,0 +1,51 @@ +""" +=========================================== +Embedding in GTK4 with a navigation toolbar +=========================================== + +Demonstrate NavigationToolbar with GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4 import ( + NavigationToolbar2GTK4 as NavigationToolbar) +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot(1, 1, 1) + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + win.set_child(vbox) + + # Add canvas to vbox + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_hexpand(True) + canvas.set_vexpand(True) + vbox.append(canvas) + + # Create toolbar + toolbar = NavigationToolbar(canvas, win) + vbox.append(toolbar) + + win.show() + + +app = Gtk.Application( + application_id='org.matplotlib.examples.EmbeddingInGTK4PanZoom') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_sgskip.py new file mode 100644 index 000000000000..c92e139de25f --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -0,0 +1,45 @@ +""" +================= +Embedding in GTK4 +================= + +Demonstrate adding a FigureCanvasGTK4Agg widget to a Gtk.ScrolledWindow using +GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot() + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + # A scrolled margin goes outside the scrollbars and viewport. + sw = Gtk.ScrolledWindow(margin_top=10, margin_bottom=10, + margin_start=10, margin_end=10) + win.set_child(sw) + + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_size_request(800, 600) + sw.set_child(canvas) + + win.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.EmbeddingInGTK4') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/gtk_spreadsheet_sgskip.py b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py similarity index 97% rename from examples/user_interfaces/gtk_spreadsheet_sgskip.py rename to examples/user_interfaces/gtk3_spreadsheet_sgskip.py index 1f0f6e702240..925ea33faa48 100644 --- a/examples/user_interfaces/gtk_spreadsheet_sgskip.py +++ b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -GTK Spreadsheet -=============== +================ +GTK3 Spreadsheet +================ Example of embedding Matplotlib in an application and interacting with a treeview to store data. Double click on an entry to update plot data. diff --git a/examples/user_interfaces/gtk4_spreadsheet_sgskip.py b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py new file mode 100644 index 000000000000..047ae4cf974e --- /dev/null +++ b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py @@ -0,0 +1,91 @@ +""" +================ +GTK4 Spreadsheet +================ + +Example of embedding Matplotlib in an application and interacting with a +treeview to store data. Double click on an entry to update plot data. +""" + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import FigureCanvas # or gtk4cairo. + +from numpy.random import random +from matplotlib.figure import Figure + + +class DataManager(Gtk.ApplicationWindow): + num_rows, num_cols = 20, 10 + + data = random((num_rows, num_cols)) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_default_size(600, 600) + + self.set_title('GtkListStore demo') + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, + spacing=8) + self.set_child(vbox) + + label = Gtk.Label(label='Double click a row to plot the data') + vbox.append(label) + + sw = Gtk.ScrolledWindow() + sw.set_has_frame(True) + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.set_hexpand(True) + sw.set_vexpand(True) + vbox.append(sw) + + model = self.create_model() + self.treeview = Gtk.TreeView(model=model) + self.treeview.connect('row-activated', self.plot_row) + sw.set_child(self.treeview) + + # Matplotlib stuff + fig = Figure(figsize=(6, 4), constrained_layout=True) + + self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea + self.canvas.set_hexpand(True) + self.canvas.set_vexpand(True) + vbox.append(self.canvas) + ax = fig.add_subplot() + self.line, = ax.plot(self.data[0, :], 'go') # plot the first row + + self.add_columns() + + def plot_row(self, treeview, path, view_column): + ind, = path # get the index into data + points = self.data[ind, :] + self.line.set_ydata(points) + self.canvas.draw() + + def add_columns(self): + for i in range(self.num_cols): + column = Gtk.TreeViewColumn(str(i), Gtk.CellRendererText(), text=i) + self.treeview.append_column(column) + + def create_model(self): + types = [float] * self.num_cols + store = Gtk.ListStore(*types) + for row in self.data: + # Gtk.ListStore.append is broken in PyGObject, so insert manually. + it = store.insert(-1) + store.set(it, {i: val for i, val in enumerate(row)}) + return store + + +def on_activate(app): + manager = DataManager(application=app) + manager.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.GTK4Spreadsheet') +app.connect('activate', on_activate) +app.run() diff --git a/examples/user_interfaces/pylab_with_gtk_sgskip.py b/examples/user_interfaces/pylab_with_gtk3_sgskip.py similarity index 96% rename from examples/user_interfaces/pylab_with_gtk_sgskip.py rename to examples/user_interfaces/pylab_with_gtk3_sgskip.py index 277f7de2a9eb..4d943032df5a 100644 --- a/examples/user_interfaces/pylab_with_gtk_sgskip.py +++ b/examples/user_interfaces/pylab_with_gtk3_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -pyplot with GTK -=============== +================ +pyplot with GTK3 +================ An example of how to use pyplot to manage your figure windows, but modify the GUI by accessing the underlying GTK widgets. diff --git a/examples/user_interfaces/pylab_with_gtk4_sgskip.py b/examples/user_interfaces/pylab_with_gtk4_sgskip.py new file mode 100644 index 000000000000..6e0cebcce23c --- /dev/null +++ b/examples/user_interfaces/pylab_with_gtk4_sgskip.py @@ -0,0 +1,51 @@ +""" +================ +pyplot with GTK4 +================ + +An example of how to use pyplot to manage your figure windows, but modify the +GUI by accessing the underlying GTK widgets. +""" + +import matplotlib +matplotlib.use('GTK4Agg') # or 'GTK4Cairo' +import matplotlib.pyplot as plt + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + + +fig, ax = plt.subplots() +ax.plot([1, 2, 3], 'ro-', label='easy as 1 2 3') +ax.plot([1, 4, 9], 'gs--', label='easy as 1 2 3 squared') +ax.legend() + +manager = fig.canvas.manager +# you can access the window or vbox attributes this way +toolbar = manager.toolbar +vbox = manager.vbox + +# now let's add a button to the toolbar +button = Gtk.Button(label='Click me') +button.connect('clicked', lambda button: print('hi mom')) +button.set_tooltip_text('Click me for fun and profit') +toolbar.append(button) + +# now let's add a widget to the vbox +label = Gtk.Label() +label.set_markup('Drag mouse over axes for position') +vbox.insert_child_after(label, fig.canvas) + + +def update(event): + if event.xdata is None: + label.set_markup('Drag mouse over axes for position') + else: + label.set_markup( + f'x,y=({event.xdata}, {event.ydata})') + + +fig.canvas.mpl_connect('motion_notify_event', update) + +plt.show() diff --git a/examples/user_interfaces/test.bmp b/examples/user_interfaces/test.bmp new file mode 100644 index 000000000000..ae58370451a8 Binary files /dev/null and b/examples/user_interfaces/test.bmp differ diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 0361a37aed48..8a6f9f62c378 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -575,24 +575,13 @@ def gen_candidates(): # rcParams deprecated and automatically mapped to another key. # Values are tuples of (version, new_name, f_old2new, f_new2old). _deprecated_map = {} - # rcParams deprecated; some can manually be mapped to another key. # Values are tuples of (version, new_name_or_None). -_deprecated_ignore_map = { - 'mpl_toolkits.legacy_colorbar': ('3.4', None), -} - +_deprecated_ignore_map = {} # rcParams deprecated; can use None to suppress warnings; remain actually -# listed in the rcParams (not included in _all_deprecated). +# listed in the rcParams. # Values are tuples of (version,) -_deprecated_remain_as_none = { - 'animation.avconv_path': ('3.3',), - 'animation.avconv_args': ('3.3',), - 'animation.html_args': ('3.3',), -} - - -_all_deprecated = {*_deprecated_map, *_deprecated_ignore_map} +_deprecated_remain_as_none = {} @docstring.Substitution( @@ -1098,8 +1087,8 @@ def use(backend, *, force=True): backend names, which are case-insensitive: - interactive backends: - GTK3Agg, GTK3Cairo, MacOSX, nbAgg, QtAgg, QtCairo, - TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo + GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, MacOSX, nbAgg, QtAgg, + QtCairo, TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo - non-interactive backends: agg, cairo, pdf, pgf, ps, svg, template diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index bbb247ec02fe..73e6163944e1 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -264,7 +264,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if bunch is not None: return bunch - font, num, symbol_name, fontsize, slanted = \ + font, num, glyph_name, fontsize, slanted = \ self._get_glyph(fontname, font_class, sym, fontsize, math) font.set_size(fontsize, dpi) @@ -292,7 +292,8 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): fontsize = fontsize, postscript_name = font.postscript_name, metrics = metrics, - symbol_name = symbol_name, + glyph_name = glyph_name, + symbol_name = glyph_name, # Backcompat alias. num = num, glyph = glyph, offset = offset @@ -358,7 +359,7 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): - symbol_name = None + glyph_name = None font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -373,13 +374,13 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): if font is not None: gid = font.get_char_index(num) if gid != 0: - symbol_name = font.get_glyph_name(gid) + glyph_name = font.get_glyph_name(gid) - if symbol_name is None: + if glyph_name is None: return self._stix_fallback._get_glyph( fontname, font_class, sym, fontsize, math) - return font, num, symbol_name, fontsize, slanted + return font, num, glyph_name, fontsize, slanted # The Bakoma fonts contain many pre-sized alternatives for the # delimiters. The AutoSizedChar class will use these alternatives @@ -556,8 +557,8 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): glyphindex = font.get_char_index(uniindex) slanted = False - symbol_name = font.get_glyph_name(glyphindex) - return font, uniindex, symbol_name, fontsize, slanted + glyph_name = font.get_glyph_name(glyphindex) + return font, uniindex, glyph_name, fontsize, slanted def get_sized_alternatives_for_symbol(self, fontname, sym): if self.cm_fallback: @@ -854,7 +855,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if found_symbol: try: - symbol_name = font.get_name_char(glyph) + glyph_name = font.get_name_char(glyph) except KeyError: _log.warning( "No glyph in standard Postscript font {!r} for {!r}" @@ -864,7 +865,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if not found_symbol: glyph = '?' num = ord(glyph) - symbol_name = font.get_name_char(glyph) + glyph_name = font.get_name_char(glyph) offset = 0 @@ -890,7 +891,8 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): fontsize = fontsize, postscript_name = font.get_fontname(), metrics = metrics, - symbol_name = symbol_name, + glyph_name = glyph_name, + symbol_name = glyph_name, # Backcompat alias. num = num, glyph = glyph, offset = offset diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 152c0ea33ff5..185ec79ca8d8 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -690,6 +690,9 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The scale factor by which the length is shrunken or expanded (default 16.0) + The PGF backend uses this argument as an RNG seed and not as + described above. Using the same seed yields the same random shape. + .. ACCEPTS: (scale: float, length: float, randomness: float) """ if scale is None: diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 802fd3c9971c..bcac6bbbf331 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -96,7 +96,7 @@ def wrapper(self, *args, **kwargs): class _TransformedBoundsLocator: """ - Axes locator for `.Axes.inset_axes` and similarly positioned axes. + Axes locator for `.Axes.inset_axes` and similarly positioned Axes. The locator is a callable object used in `.Axes.set_aspect` to compute the axes location depending on the renderer. @@ -105,7 +105,7 @@ class _TransformedBoundsLocator: def __init__(self, bounds, transform): """ *bounds* (a ``[l, b, w, h]`` rectangle) and *transform* together - specify the position of the inset axes. + specify the position of the inset Axes. """ self._bounds = bounds self._transform = transform @@ -563,15 +563,15 @@ def __init__(self, fig, rect, **kwargs ): """ - Build an axes in a figure. + Build an Axes in a figure. Parameters ---------- fig : `~matplotlib.figure.Figure` - The axes is build in the `.Figure` *fig*. + The Axes is built in the `.Figure` *fig*. rect : [left, bottom, width, height] - The axes is build in the rectangle *rect*. *rect* is in + The Axes is built in the rectangle *rect*. *rect* is in `.Figure` coordinates. sharex, sharey : `~.axes.Axes`, optional @@ -579,10 +579,10 @@ def __init__(self, fig, rect, y axis in the input `~.axes.Axes`. frameon : bool, default: True - Whether the axes frame is visible. + Whether the Axes frame is visible. box_aspect : float, optional - Set a fixed aspect for the axes box, i.e. the ratio of height to + Set a fixed aspect for the Axes box, i.e. the ratio of height to width. See `~.axes.Axes.set_box_aspect` for details. **kwargs @@ -615,7 +615,7 @@ def __init__(self, fig, rect, self.set_figure(fig) self.set_box_aspect(box_aspect) self._axes_locator = None # Optionally set via update(kwargs). - # placeholder for any colorbars added that use this axes. + # placeholder for any colorbars added that use this Axes. # (see colorbar.py): self._colorbars = [] self.spines = mspines.Spines.from_dict(self._gen_axes_spines()) @@ -723,7 +723,7 @@ def __repr__(self): def get_window_extent(self, *args, **kwargs): """ - Return the axes bounding box in display space; *args* and *kwargs* + Return the Axes bounding box in display space; *args* and *kwargs* are empty. This bounding box does not include the spines, ticks, ticklables, @@ -809,7 +809,7 @@ def _set_lim_and_transforms(self): This method is primarily used by rectilinear projections of the `~matplotlib.axes.Axes` class, and is meant to be overridden by - new kinds of projection axes that need different transformations + new kinds of projection Axes that need different transformations and limits. (See `~matplotlib.projections.polar.PolarAxes` for an example.) """ @@ -866,7 +866,7 @@ def get_xaxis_text1_transform(self, pad_points): ------- transform : Transform The transform used for drawing x-axis labels, which will add - *pad_points* of padding (in points) between the axes and the label. + *pad_points* of padding (in points) between the axis and the label. The x-direction is in data coordinates and the y-direction is in axis coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -892,7 +892,7 @@ def get_xaxis_text2_transform(self, pad_points): ------- transform : Transform The transform used for drawing secondary x-axis labels, which will - add *pad_points* of padding (in points) between the axes and the + add *pad_points* of padding (in points) between the axis and the label. The x-direction is in data coordinates and the y-direction is in axis coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -942,7 +942,7 @@ def get_yaxis_text1_transform(self, pad_points): ------- transform : Transform The transform used for drawing y-axis labels, which will add - *pad_points* of padding (in points) between the axes and the label. + *pad_points* of padding (in points) between the axis and the label. The x-direction is in axis coordinates and the y-direction is in data coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -968,7 +968,7 @@ def get_yaxis_text2_transform(self, pad_points): ------- transform : Transform The transform used for drawing secondart y-axis labels, which will - add *pad_points* of padding (in points) between the axes and the + add *pad_points* of padding (in points) between the axis and the label. The x-direction is in axis coordinates and the y-direction is in data coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -1002,7 +1002,7 @@ def _update_transScale(self): def get_position(self, original=False): """ - Get a copy of the axes rectangle as a `.Bbox`. + Return the position of the Axes within the figure as a `.Bbox`. Parameters ---------- @@ -1026,7 +1026,7 @@ def get_position(self, original=False): def set_position(self, pos, which='both'): """ - Set the axes position. + Set the Axes position. Axes have two position attributes. The 'original' position is the position allocated for the Axes. The 'active' position is the @@ -1081,7 +1081,7 @@ def reset_position(self): def set_axes_locator(self, locator): """ - Set the axes locator. + Set the Axes locator. Parameters ---------- @@ -1097,7 +1097,7 @@ def get_axes_locator(self): return self._axes_locator def _set_artist_props(self, a): - """Set the boilerplate props for artists added to axes.""" + """Set the boilerplate props for artists added to Axes.""" a.set_figure(self.figure) if not a.is_transform_set(): a.set_transform(self.transData) @@ -1111,8 +1111,8 @@ def _gen_axes_patch(self): Returns ------- Patch - The patch used to draw the background of the axes. It is also used - as the clipping path for any data elements on the axes. + The patch used to draw the background of the Axes. It is also used + as the clipping path for any data elements on the Axes. In the standard axes, this is a rectangle, but in other projections it may not be. @@ -1129,9 +1129,9 @@ def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): ------- dict Mapping of spine names to `.Line2D` or `.Patch` instances that are - used to draw axes spines. + used to draw Axes spines. - In the standard axes, spines are single line segments, but in other + In the standard Axes, spines are single line segments, but in other projections they may not be. Notes @@ -1147,7 +1147,7 @@ def sharex(self, other): This is equivalent to passing ``sharex=other`` when constructing the axes, and cannot be used if the x-axis is already being shared with - another axes. + another Axes. """ _api.check_isinstance(_AxesBase, other=other) if self._sharex is not None and other is not self._sharex: @@ -1166,7 +1166,7 @@ def sharey(self, other): This is equivalent to passing ``sharey=other`` when constructing the axes, and cannot be used if the y-axis is already being shared with - another axes. + another Axes. """ _api.check_isinstance(_AxesBase, other=other) if self._sharey is not None and other is not self._sharey: @@ -1180,7 +1180,7 @@ def sharey(self, other): self.yaxis._scale = other.yaxis._scale def cla(self): - """Clear the axes.""" + """Clear the Axes.""" # Note: this is called by Axes.__init__() # stash the current visibility state @@ -1285,7 +1285,7 @@ def cla(self): for _title in (self.title, self._left_title, self._right_title): self._set_artist_props(_title) - # The patch draws the background of the axes. We want this to be below + # The patch draws the background of the Axes. We want this to be below # 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() @@ -1471,7 +1471,7 @@ def texts(self): valid_types=mtext.Text) def clear(self): - """Clear the axes.""" + """Clear the Axes.""" self.cla() def get_facecolor(self): @@ -1520,13 +1520,13 @@ def set_prop_cycle(self, *args, **kwargs): Form 2 creates a `~cycler.Cycler` which cycles over one or more properties simultaneously and set it as the property cycle of the - axes. If multiple properties are given, their value lists must have + Axes. If multiple properties are given, their value lists must have the same length. This is just a shortcut for explicitly creating a cycler and passing it to the function, i.e. it's short for ``set_prop_cycle(cycler(label=values label2=values2, ...))``. Form 3 creates a `~cycler.Cycler` for a single property and set it - as the property cycle of the axes. This form exists for compatibility + as the property cycle of the Axes. This form exists for compatibility with the original `cycler.cycler` interface. Its use is discouraged in favor of the kwarg form, i.e. ``set_prop_cycle(label=values)``. @@ -1710,7 +1710,7 @@ def set_adjustable(self, adjustable, share=False): for ax in axs)): # Limits adjustment by apply_aspect assumes that the axes' aspect # ratio can be computed from the data limits and scales. - raise ValueError("Cannot set axes adjustable to 'datalim' for " + raise ValueError("Cannot set Axes adjustable to 'datalim' for " "Axes which override 'get_data_ratio'") for ax in axs: ax._adjustable = adjustable @@ -1718,7 +1718,7 @@ def set_adjustable(self, adjustable, share=False): def get_box_aspect(self): """ - Return the axes box aspect, i.e. the ratio of height to width. + Return the Axes box aspect, i.e. the ratio of height to width. The box aspect is ``None`` (i.e. chosen depending on the available figure space) unless explicitly specified. @@ -1734,21 +1734,21 @@ def get_box_aspect(self): def set_box_aspect(self, aspect=None): """ - Set the axes box aspect, i.e. the ratio of height to width. + Set the Axes box aspect, i.e. the ratio of height to width. - This defines the aspect of the axes in figure space and is not to be + This defines the aspect of the Axes in figure space and is not to be confused with the data aspect (see `~.Axes.set_aspect`). Parameters ---------- aspect : float or None Changes the physical dimensions of the Axes, such that the ratio - of the axes height to the axes width in physical units is equal to + of the Axes height to the Axes width in physical units is equal to *aspect*. Defining a box aspect will change the *adjustable* property to 'datalim' (see `~.Axes.set_adjustable`). *None* will disable a fixed box aspect so that height and width - of the axes are chosen independently. + of the Axes are chosen independently. See Also -------- @@ -2138,7 +2138,7 @@ def _sci(self, im): This image will be the target of colormap functions like `~.pyplot.viridis`, and other functions such as `~.pyplot.clim`. The - current image is an attribute of the current axes. + current image is an attribute of the current Axes. """ _api.check_isinstance( (mpl.contour.ContourSet, mcoll.Collection, mimage.AxesImage), @@ -2157,7 +2157,7 @@ def _gci(self): def has_data(self): """ - Return whether any artists have been added to the axes. + Return whether any artists have been added to the Axes. This should not be used to determine whether the *dataLim* need to be updated, and may not actually be useful for @@ -2204,12 +2204,12 @@ def add_artist(self, a): def add_child_axes(self, ax): """ - Add an `.AxesBase` to the axes' children; return the child axes. + Add an `.AxesBase` to the Axes' children; return the child Axes. This is the lowlevel version. See `.axes.Axes.inset_axes`. """ - # normally axes have themselves as the axes, but these need to have + # normally Axes have themselves as the Axes, but these need to have # their parent... # Need to bypass the getter... ax._axes = self @@ -2612,7 +2612,7 @@ def use_sticky_edges(self): @use_sticky_edges.setter def use_sticky_edges(self, b): self._use_sticky_edges = bool(b) - # No effect until next autoscaling, which will mark the axes as stale. + # No effect until next autoscaling, which will mark the Axes as stale. def set_xmargin(self, m): """ @@ -2664,7 +2664,7 @@ def margins(self, *margins, x=None, y=None, tight=True): """ Set or retrieve autoscaling margins. - The padding added to each limit of the axes is the *margin* + The padding added to each limit of the Axes is the *margin* times the data interval. All input parameters must be floats within the range [0, 1]. Passing both positional and keyword arguments is invalid and will raise a TypeError. If no @@ -2766,7 +2766,7 @@ def autoscale(self, enable=True, axis='both', tight=None): Convenience method for simple axis view autoscaling. It turns autoscaling on or off, and then, if autoscaling for either axis is on, it performs - the autoscaling on the specified axis or axes. + the autoscaling on the specified axis or Axes. Parameters ---------- @@ -2833,7 +2833,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): case, use :meth:`matplotlib.axes.Axes.relim` prior to calling autoscale_view. - If the views of the axes are fixed, e.g. via `set_xlim`, they will + If the views of the Axes are fixed, e.g. via `set_xlim`, they will not be changed by autoscale_view(). See :meth:`matplotlib.axes.Axes.autoscale` for an alternative. """ @@ -2842,9 +2842,9 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): x_stickies = y_stickies = np.array([]) if self.use_sticky_edges: - # Only iterate over axes and artists if needed. The check for + # Only iterate over Axes and artists if needed. The check for # ``hasattr(ax, "_children")`` is necessary because this can be - # called very early in the axes init process (e.g., for twin axes) + # called very early in the Axes init process (e.g., for twin axes) # when these attributes don't even exist yet, in which case # `get_children` would raise an AttributeError. if self._xmargin and scalex and self._autoscaleXon: @@ -2948,7 +2948,7 @@ def _get_axis_map(self): and the r-axis is still named "y" (for back-compatibility). In practice, this means that the entries are typically "x" and "y", and - additionally "z" for 3D axes. + additionally "z" for 3D Axes. """ return dict(zip(self._axis_names, self._get_axis_list())) @@ -2989,9 +2989,9 @@ def _update_title_position(self, renderer): if bb is not None: top = max(top, bb.ymax) if top < 0: - # the top of axes is not even on the figure, so don't try and + # the top of Axes is not even on the figure, so don't try and # automatically place it. - _log.debug('top of axes not in the figure, so title not moved') + _log.debug('top of Axes not in the figure, so title not moved') return if title.get_window_extent(renderer).ymin < top: _, y = self.transAxes.inverted().transform((0, top)) @@ -3024,7 +3024,7 @@ def draw(self, renderer): # prevent triggering call backs during the draw process self._stale = True - # loop over self and child axes... + # loop over self and child Axes... locator = self.get_axes_locator() if locator: pos = locator(self, renderer) @@ -3035,7 +3035,7 @@ def draw(self, renderer): artists = self.get_children() artists.remove(self.patch) - # the frame draws the edges around the axes patch -- we + # the frame draws the edges around the Axes patch -- we # decouple these so the patch can be in the background and the # frame in the foreground. Do this before drawing the axis # objects so that the spine has the opportunity to update them. @@ -3119,12 +3119,12 @@ def get_renderer_cache(self): # Axes rectangle characteristics def get_frame_on(self): - """Get whether the axes rectangle patch is drawn.""" + """Get whether the Axes rectangle patch is drawn.""" return self._frameon def set_frame_on(self, b): """ - Set whether the axes rectangle patch is drawn. + Set whether the Axes rectangle patch is drawn. Parameters ---------- @@ -3238,7 +3238,7 @@ def ticklabel_format(self, *, axis='both', style='', scilimits=None, Parameters ---------- axis : {'x', 'y', 'both'}, default: 'both' - The axes to configure. Only major ticks are affected. + The axis to configure. Only major ticks are affected. style : {'sci', 'scientific', 'plain'} Whether to use scientific notation. @@ -3463,7 +3463,7 @@ def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, The label text. labelpad : float, default: :rc:`axes.labelpad` - Spacing in points from the axes bounding box including ticks + Spacing in points from the Axes bounding box including ticks and tick labels. If None, the previous value is left as is. loc : {'left', 'center', 'right'}, default: :rc:`xaxis.labellocation` @@ -3533,7 +3533,7 @@ def set_xbound(self, lower=None, upper=None): """ Set the lower and upper numerical bounds of the x-axis. - This method will honor axes inversion regardless of parameter order. + This method will honor axis inversion regardless of parameter order. It will not change the autoscaling setting (`.get_autoscalex_on()`). Parameters @@ -3807,7 +3807,7 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, The label text. labelpad : float, default: :rc:`axes.labelpad` - Spacing in points from the axes bounding box including ticks + Spacing in points from the Axes bounding box including ticks and tick labels. If None, the previous value is left as is. loc : {'bottom', 'center', 'top'}, default: :rc:`yaxis.labellocation` @@ -3877,7 +3877,7 @@ def set_ybound(self, lower=None, upper=None): """ Set the lower and upper numerical bounds of the y-axis. - This method will honor axes inversion regardless of parameter order. + This method will honor axis inversion regardless of parameter order. It will not change the autoscaling setting (`.get_autoscaley_on()`). Parameters @@ -4153,7 +4153,7 @@ def format_coord(self, x, y): def minorticks_on(self): """ - Display minor ticks on the axes. + Display minor ticks on the Axes. Displaying minor ticks may reduce performance; you may turn them off using `minorticks_off()` if drawing speed is a problem. @@ -4171,7 +4171,7 @@ def minorticks_on(self): ax.set_minor_locator(mticker.AutoMinorLocator()) def minorticks_off(self): - """Remove minor ticks from the axes.""" + """Remove minor ticks from the Axes.""" self.xaxis.set_minor_locator(mticker.NullLocator()) self.yaxis.set_minor_locator(mticker.NullLocator()) @@ -4179,25 +4179,25 @@ def minorticks_off(self): def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. + Return whether this Axes supports the zoom box button functionality. """ return True def can_pan(self): """ - Return whether this axes supports any pan/zoom button functionality. + Return whether this Axes supports any pan/zoom button functionality. """ return True def get_navigate(self): """ - Get whether the axes responds to navigation commands + Get whether the Axes responds to navigation commands. """ return self._navigate def set_navigate(self, b): """ - Set whether the axes responds to navigation toolbar commands + Set whether the Axes responds to navigation toolbar commands. Parameters ---------- @@ -4207,13 +4207,13 @@ def set_navigate(self, b): def get_navigate_mode(self): """ - Get the navigation toolbar button status: 'PAN', 'ZOOM', or None + Get the navigation toolbar button status: 'PAN', 'ZOOM', or None. """ return self._navigate_mode def set_navigate_mode(self, b): """ - Set the navigation toolbar button status; + Set the navigation toolbar button status. .. warning :: this is not a user-API function. @@ -4346,7 +4346,7 @@ def _set_view_from_bbox(self, bbox, direction='in', [xmin0, xmax0, xmin, xmax]) # To screen space. factor = (sxmax0 - sxmin0) / (sxmax - sxmin) # Unzoom factor. # Move original bounds away by - # (factor) x (distance between unzoom box and axes bbox). + # (factor) x (distance between unzoom box and Axes bbox). sxmin1 = sxmin0 - factor * (sxmin - sxmin0) sxmax1 = sxmax0 + factor * (sxmax0 - sxmax) # And back to data space. @@ -4542,7 +4542,7 @@ def get_tightbbox(self, renderer, call_axes_locator=True, bbox_extra_artists : list of `.Artist` or ``None`` List of artists to include in the tight bounding box. If - ``None`` (default), then all artist children of the axes are + ``None`` (default), then all artist children of the Axes are included in the tight bounding box. call_axes_locator : bool, default: True @@ -4550,7 +4550,7 @@ def get_tightbbox(self, renderer, call_axes_locator=True, ``_axes_locator`` attribute, which is necessary to get the correct bounding box. ``call_axes_locator=False`` can be used if the caller is only interested in the relative size of the tightbbox - compared to the axes bbox. + compared to the Axes bbox. for_layout_only : default: False The bounding box will *not* include the x-extent of the title and @@ -4620,14 +4620,14 @@ def get_tightbbox(self, renderer, call_axes_locator=True, for a in bbox_artists: # Extra check here to quickly see if clipping is on and - # contained in the axes. If it is, don't get the tightbbox for + # contained in the Axes. If it is, don't get the tightbbox for # this artist because this can be expensive: clip_extent = a._get_clipping_extent_bbox() if clip_extent is not None: clip_extent = mtransforms.Bbox.intersection( clip_extent, axbbox) if np.all(clip_extent.extents == axbbox.extents): - # clip extent is inside the axes bbox so don't check + # clip extent is inside the Axes bbox so don't check # this artist continue bbox = a.get_tightbbox(renderer) @@ -4639,7 +4639,7 @@ def get_tightbbox(self, renderer, call_axes_locator=True, [b for b in bb if b.width != 0 or b.height != 0]) def _make_twin_axes(self, *args, **kwargs): - """Make a twinx axes of self. This is used for twinx and twiny.""" + """Make a twinx Axes of self. This is used for twinx and twiny.""" # Typically, SubplotBase._make_twin_axes is called instead of this. if 'sharex' in kwargs and 'sharey' in kwargs: raise ValueError("Twinned Axes may share only one axis") @@ -4670,7 +4670,7 @@ def twinx(self): Notes ----- For those who are 'picking' artists while using twinx, pick - events are only called for the artists in the top-most axes. + events are only called for the artists in the top-most Axes. """ ax2 = self._make_twin_axes(sharex=self) ax2.yaxis.tick_right() @@ -4700,7 +4700,7 @@ def twiny(self): Notes ----- For those who are 'picking' artists while using twiny, pick - events are only called for the artists in the top-most axes. + events are only called for the artists in the top-most Axes. """ ax2 = self._make_twin_axes(sharey=self) ax2.xaxis.tick_top() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 21ff162062d0..fda7bd1c9613 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -101,6 +101,7 @@ def _safe_pyplot_import(): backend_mapping = { 'qt': 'qtagg', 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', 'wx': 'wxagg', 'tk': 'tkagg', 'macosx': 'macosx', @@ -1211,9 +1212,10 @@ def _on_timer(self): class Event: """ - A Matplotlib event. Attach additional attributes as defined in - :meth:`FigureCanvasBase.mpl_connect`. The following attributes - are defined and shown with their default values + A Matplotlib event. + + The following attributes are defined and shown with their default values. + Subclasses may define additional attributes. Attributes ---------- @@ -1232,20 +1234,20 @@ def __init__(self, name, canvas, guiEvent=None): class DrawEvent(Event): """ - An event triggered by a draw operation on the canvas + An event triggered by a draw operation on the canvas. - In most backends callbacks subscribed to this callback will be - fired after the rendering is complete but before the screen is - updated. Any extra artists drawn to the canvas's renderer will - be reflected without an explicit call to ``blit``. + In most backends, callbacks subscribed to this event will be fired after + the rendering is complete but before the screen is updated. Any extra + artists drawn to the canvas's renderer will be reflected without an + explicit call to ``blit``. .. warning:: Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may not be safe with all backends and may cause infinite recursion. - In addition to the `Event` attributes, the following event - attributes are defined: + A DrawEvent has a number of special attributes in addition to those defined + by the parent `Event` class. Attributes ---------- @@ -1259,10 +1261,10 @@ def __init__(self, name, canvas, renderer): class ResizeEvent(Event): """ - An event triggered by a canvas resize + An event triggered by a canvas resize. - In addition to the `Event` attributes, the following event - attributes are defined: + A ResizeEvent has a number of special attributes in addition to those + defined by the parent `Event` class. Attributes ---------- @@ -1284,32 +1286,23 @@ class LocationEvent(Event): """ An event that has a screen location. - The following additional attributes are defined and shown with - their default values. - - In addition to the `Event` attributes, the following - event attributes are defined: + A LocationEvent has a number of special attributes in addition to those + defined by the parent `Event` class. Attributes ---------- - x : int - x position - pixels from left of canvas. - y : int - y position - pixels from bottom of canvas. + x, y : int or None + Event location in pixels from bottom left of canvas. inaxes : `~.axes.Axes` or None The `~.axes.Axes` instance over which the mouse is, if any. - xdata : float or None - x data coordinate of the mouse. - ydata : float or None - y data coordinate of the mouse. + xdata, ydata : float or None + Data coordinates of the mouse within *inaxes*, or *None* if the mouse + is not over an Axes. """ lastevent = None # the last event that was triggered before this one def __init__(self, name, canvas, x, y, guiEvent=None): - """ - (*x*, *y*) in figure coords ((0, 0) = bottom left). - """ super().__init__(name, canvas, guiEvent=guiEvent) # x position - pixels from left of canvas self.x = int(x) if x is not None else x @@ -1378,13 +1371,11 @@ class MouseButton(IntEnum): class MouseEvent(LocationEvent): """ - A mouse event ('button_press_event', - 'button_release_event', - 'scroll_event', - 'motion_notify_event'). + A mouse event ('button_press_event', 'button_release_event', \ +'scroll_event', 'motion_notify_event'). - In addition to the `Event` and `LocationEvent` - attributes, the following attributes are defined: + A MouseEvent has a number of special attributes in addition to those + defined by the parent `Event` and `LocationEvent` classes. Attributes ---------- @@ -1426,10 +1417,6 @@ def on_press(event): def __init__(self, name, canvas, x, y, button=None, key=None, step=0, dblclick=False, guiEvent=None): - """ - (*x*, *y*) in figure coords ((0, 0) = bottom left) - button pressed None, 1, 2, 3, 'up', 'down' - """ if button in MouseButton.__members__.values(): button = MouseButton(button) self.button = button @@ -1450,11 +1437,14 @@ def __str__(self): class PickEvent(Event): """ - A pick event, fired when the user picks a location on the canvas + A pick event. + + This event is fired when the user picks a location on the canvas sufficiently close to an artist that has been made pickable with `.Artist.set_picker`. - Attrs: all the `Event` attributes plus + A PickEvent has a number of special attributes in addition to those defined + by the parent `Event` class. Attributes ---------- @@ -1495,19 +1485,16 @@ class KeyEvent(LocationEvent): """ A key event (key press, key release). - Attach additional attributes as defined in - :meth:`FigureCanvasBase.mpl_connect`. - - In addition to the `Event` and `LocationEvent` - attributes, the following attributes are defined: + A KeyEvent has a number of special attributes in addition to those defined + by the parent `Event` and `LocationEvent` classes. Attributes ---------- key : None or str - the key(s) pressed. Could be **None**, a single case sensitive ascii - character ("g", "G", "#", etc.), a special key - ("control", "shift", "f1", "up", etc.) or a - combination of the above (e.g., "ctrl+alt+g", "ctrl+alt+G"). + The key(s) pressed. Could be *None*, a single case sensitive Unicode + character ("g", "G", "#", etc.), a special key ("control", "shift", + "f1", "up", etc.) or a combination of the above (e.g., "ctrl+alt+g", + "ctrl+alt+G"). Notes ----- @@ -1570,7 +1557,7 @@ def _draw(renderer): raise Done(renderer) def _no_output_draw(figure): # _no_output_draw was promoted to the figure level, but # keep this here in case someone was calling it... - figure.draw_no_output() + figure.draw_without_rendering() def _is_non_interactive_terminal_ipython(ip): @@ -1670,7 +1657,7 @@ class FigureCanvasBase: A high-level figure instance. """ - # Set to one of {"qt", "gtk3", "wx", "tk", "macosx"} if an + # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an # interactive framework is required, or None otherwise. required_interactive_framework = None @@ -1746,7 +1733,7 @@ def _fix_ipython_backend2gui(cls): # don't break on our side. return rif = getattr(cls, "required_interactive_framework", None) - backend2gui_rif = {"qt": "qt", "gtk3": "gtk3", + backend2gui_rif = {"qt": "qt", "gtk3": "gtk3", "gtk4": "gtk4", "wx": "wx", "macosx": "osx"}.get(rif) if backend2gui_rif: if _is_non_interactive_terminal_ipython(ip): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index cc81b1f9269b..390b90134aed 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -900,7 +900,7 @@ class ToolHelpBase(ToolBase): @staticmethod def format_shortcut(key_sequence): """ - Converts a shortcut string from the notation used in rc config to the + Convert a shortcut string from the notation used in rc config to the standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. """ return (key_sequence if len(key_sequence) == 1 else diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py new file mode 100644 index 000000000000..f652815f5120 --- /dev/null +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -0,0 +1,174 @@ +""" +Common code for GTK3 and GTK4 backends. +""" + +import logging + +import matplotlib as mpl +from matplotlib import backend_tools, cbook +from matplotlib.backend_bases import ( + _Backend, NavigationToolbar2, TimerBase, +) + +# The GTK3/GTK4 backends will have already called `gi.require_version` to set +# the desired GTK. +from gi.repository import Gio, GLib, Gtk + + +_log = logging.getLogger(__name__) + +backend_version = "%s.%s.%s" % ( + Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) + +# Placeholder +_application = None + + +def _shutdown_application(app): + # The application might prematurely shut down if Ctrl-C'd out of IPython, + # so close all windows. + for win in app.get_windows(): + win.close() + # The PyGObject wrapper incorrectly thinks that None is not allowed, or we + # would call this: + # Gio.Application.set_default(None) + # Instead, we set this property and ignore default applications with it: + app._created_by_matplotlib = True + global _application + _application = None + + +def _create_application(): + global _application + + if _application is None: + app = Gio.Application.get_default() + if app is None or getattr(app, '_created_by_matplotlib'): + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + _application = Gtk.Application.new('org.matplotlib.Matplotlib3', + Gio.ApplicationFlags.NON_UNIQUE) + # The activate signal must be connected, but we don't care for + # handling it, since we don't do any remote processing. + _application.connect('activate', lambda *args, **kwargs: None) + _application.connect('shutdown', _shutdown_application) + _application.register() + cbook._setup_new_guiapp() + else: + _application = app + + return _application + + +class TimerGTK(TimerBase): + """Subclass of `.TimerBase` using GTK timer events.""" + + def __init__(self, *args, **kwargs): + self._timer = None + super().__init__(*args, **kwargs) + + def _timer_start(self): + # Need to stop it, otherwise we potentially leak a timer id that will + # never be stopped. + self._timer_stop() + self._timer = GLib.timeout_add(self._interval, self._on_timer) + + def _timer_stop(self): + if self._timer is not None: + GLib.source_remove(self._timer) + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started. + if self._timer is not None: + self._timer_stop() + self._timer_start() + + def _on_timer(self): + super()._on_timer() + + # Gtk timeout_add() requires that the callback returns True if it + # is to be called again. + if self.callbacks and not self._single: + return True + else: + self._timer = None + return False + + +class _NavigationToolbar2GTK(NavigationToolbar2): + # Must be implemented in GTK3/GTK4 backends: + # * __init__ + # * save_figure + + def set_message(self, s): + escaped = GLib.markup_escape_text(s) + self.message.set_markup(f'{escaped}') + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas._draw_rubberband(rect) + + def remove_rubberband(self): + self.canvas._draw_rubberband(None) + + def _update_buttons_checked(self): + for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: + button = self._gtk_ids.get(name) + if button: + with button.handler_block(button._signal_handler): + button.set_active(self.mode.name == active) + + def pan(self, *args): + super().pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super().zoom(*args) + self._update_buttons_checked() + + def set_history_buttons(self): + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 + if 'Back' in self._gtk_ids: + self._gtk_ids['Back'].set_sensitive(can_backward) + if 'Forward' in self._gtk_ids: + self._gtk_ids['Forward'].set_sensitive(can_forward) + + +class RubberbandGTK(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + _NavigationToolbar2GTK.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) + + def remove_rubberband(self): + _NavigationToolbar2GTK.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) + + +class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase, Gtk.Window): + def _get_canvas(self, fig): + return self.canvas.__class__(fig) + + def trigger(self, *args): + _NavigationToolbar2GTK.configure_subplots( + self._make_classic_style_pseudo_toolbar(), None) + + +class _BackendGTK(_Backend): + @staticmethod + def mainloop(): + global _application + if _application is None: + return + + try: + _application.run() # Quits when all added windows close. + finally: + # Running after quit is undefined, so create a new one next time. + _application = None diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 46b48c2200aa..efbbbb387238 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -29,13 +29,17 @@ raise ImportError from e from gi.repository import Gio, GLib, GObject, Gtk, Gdk +from ._backend_gtk import ( + _create_application, _shutdown_application, + backend_version, _BackendGTK, _NavigationToolbar2GTK, + TimerGTK as TimerGTK3, + ConfigureSubplotsGTK as ConfigureSubplotsGTK3, + RubberbandGTK as RubberbandGTK3, +) _log = logging.getLogger(__name__) -backend_version = "%s.%s.%s" % ( - Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) - @_api.caching_module_getattr # module-level deprecations class __getattr__: @@ -55,45 +59,12 @@ def cursord(self): except TypeError as exc: return {} - -# Placeholder -_application = None - - -def _shutdown_application(app): - # The application might prematurely shut down if Ctrl-C'd out of IPython, - # so close all windows. - for win in app.get_windows(): - win.destroy() - # The PyGObject wrapper incorrectly thinks that None is not allowed, or we - # would call this: - # Gio.Application.set_default(None) - # Instead, we set this property and ignore default applications with it: - app._created_by_matplotlib = True - global _application - _application = None - - -def _create_application(): - global _application - - if _application is None: - app = Gio.Application.get_default() - if app is None or getattr(app, '_created_by_matplotlib'): - # display_is_valid returns False only if on Linux and neither X11 - # nor Wayland display can be opened. - if not mpl._c_internal_utils.display_is_valid(): - raise RuntimeError('Invalid DISPLAY variable') - _application = Gtk.Application.new('org.matplotlib.Matplotlib3', - Gio.ApplicationFlags.NON_UNIQUE) - # The activate signal must be connected, but we don't care for - # handling it, since we don't do any remote processing. - _application.connect('activate', lambda *args, **kwargs: None) - _application.connect('shutdown', _shutdown_application) - _application.register() - cbook._setup_new_guiapp() - else: - _application = app + icon_filename = _api.deprecated("3.6", obj_type="")(property( + lambda self: + "matplotlib.png" if sys.platform == "win32" else "matplotlib.svg")) + window_icon = _api.deprecated("3.6", obj_type="")(property( + lambda self: + str(cbook._get_data_path("images", __getattr__("icon_filename"))))) @functools.lru_cache() @@ -110,42 +81,6 @@ def _mpl_to_gtk_cursor(mpl_cursor): return Gdk.Cursor.new_from_name(Gdk.Display.get_default(), name) -class TimerGTK3(TimerBase): - """Subclass of `.TimerBase` using GTK3 timer events.""" - - def __init__(self, *args, **kwargs): - self._timer = None - super().__init__(*args, **kwargs) - - def _timer_start(self): - # Need to stop it, otherwise we potentially leak a timer id that will - # never be stopped. - self._timer_stop() - self._timer = GLib.timeout_add(self._interval, self._on_timer) - - def _timer_stop(self): - if self._timer is not None: - GLib.source_remove(self._timer) - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - def _on_timer(self): - super()._on_timer() - - # Gtk timeout_add() requires that the callback returns True if it - # is to be called again. - if self.callbacks and not self._single: - return True - else: - self._timer = None - return False - - class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk3" _timer_cls = TimerGTK3 @@ -365,7 +300,7 @@ class FigureManagerGTK3(FigureManagerBase): num : int or str The Figure number toolbar : Gtk.Toolbar - The Gtk.Toolbar + The toolbar vbox : Gtk.VBox The Gtk.VBox containing the canvas and toolbar window : Gtk.Window @@ -373,19 +308,15 @@ class FigureManagerGTK3(FigureManagerBase): """ def __init__(self, canvas, num): - _create_application() + app = _create_application() self.window = Gtk.Window() - _application.add_window(self.window) + app.add_window(self.window) super().__init__(canvas, num) self.window.set_wmclass("matplotlib", "Matplotlib") - try: - self.window.set_icon_from_file(window_icon) - except Exception: - # Some versions of gtk throw a glib.GError but not all, so I am not - # sure how to catch it. I am unhappy doing a blanket catch here, - # but am not sure what a better way is - JDH - _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) + icon_ext = "png" if sys.platform == "win32" else "svg" + self.window.set_icon_from_file( + str(cbook._get_data_path(f"images/matplotlib.{icon_ext}"))) self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) @@ -492,7 +423,7 @@ def resize(self, width, height): self.window.resize(width, height) -class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): +class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar): def __init__(self, canvas, window): self.win = window GObject.GObject.__init__(self) @@ -509,21 +440,16 @@ def __init__(self, canvas, window): str(cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) - self._gtk_ids[text] = tbutton = ( + self._gtk_ids[text] = button = ( Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self.insert(tbutton, -1) + button.set_label(text) + button.set_icon_widget(image) # Save the handler id, so that we can block it as needed. - tbutton._signal_handler = tbutton.connect( + button._signal_handler = button.connect( 'clicked', getattr(self, callback)) - tbutton.set_tooltip_text(tooltip_text) - - toolitem = Gtk.SeparatorToolItem() - self.insert(toolitem, -1) - toolitem.set_draw(False) - toolitem.set_expand(True) + button.set_tooltip_text(tooltip_text) + self.insert(button, -1) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers @@ -534,6 +460,7 @@ def __init__(self, canvas, window): label = Gtk.Label() label.set_markup( '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + toolitem.set_expand(True) # Push real message to the right. toolitem.add(label) toolitem = Gtk.ToolItem() @@ -545,35 +472,6 @@ def __init__(self, canvas, window): NavigationToolbar2.__init__(self, canvas) - def set_message(self, s): - escaped = GLib.markup_escape_text(s) - self.message.set_markup(f'{escaped}') - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] - self.canvas._draw_rubberband(rect) - - def remove_rubberband(self): - self.canvas._draw_rubberband(None) - - def _update_buttons_checked(self): - for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: - button = self._gtk_ids.get(name) - if button: - with button.handler_block(button._signal_handler): - button.set_active(self.mode.name == active) - - def pan(self, *args): - super().pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super().zoom(*args) - self._update_buttons_checked() - def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", @@ -618,14 +516,6 @@ def on_notify_filter(*args): except Exception as e: error_msg_gtk(str(e), parent=self) - def set_history_buttons(self): - can_backward = self._nav_stack._pos > 0 - can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 - if 'Back' in self._gtk_ids: - self._gtk_ids['Back'].set_sensitive(can_backward) - if 'Forward' in self._gtk_ids: - self._gtk_ids['Forward'].set_sensitive(can_forward) - class ToolbarGTK3(ToolContainerBase, Gtk.Box): _icon_extension = '-symbolic.svg' @@ -643,26 +533,26 @@ def __init__(self, toolmanager): def add_toolitem(self, name, group, position, image_file, description, toggle): if toggle: - tbutton = Gtk.ToggleToolButton() + button = Gtk.ToggleToolButton() else: - tbutton = Gtk.ToolButton() - tbutton.set_label(name) + button = Gtk.ToolButton() + button.set_label(name) if image_file is not None: image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string(image_file), Gtk.IconSize.LARGE_TOOLBAR) - tbutton.set_icon_widget(image) + button.set_icon_widget(image) if position is None: position = -1 - self._add_button(tbutton, group, position) - signal = tbutton.connect('clicked', self._call_tool, name) - tbutton.set_tooltip_text(description) - tbutton.show_all() + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + button.show_all() self._toolitems.setdefault(name, []) - self._toolitems[name].append((tbutton, signal)) + self._toolitems[name].append((button, signal)) def _add_button(self, button, group, position): if group not in self._groups: @@ -707,16 +597,6 @@ def set_message(self, s): self._message.set_label(s) -class RubberbandGTK3(backend_tools.RubberbandBase): - def draw_rubberband(self, x0, y0, x1, y1): - NavigationToolbar2GTK3.draw_rubberband( - self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) - - def remove_rubberband(self): - NavigationToolbar2GTK3.remove_rubberband( - self._make_classic_style_pseudo_toolbar()) - - class SaveFigureGTK3(backend_tools.SaveFigureBase): def trigger(self, *args, **kwargs): @@ -733,15 +613,6 @@ def set_cursor(self, cursor): self._make_classic_style_pseudo_toolbar(), cursor) -class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): - def _get_canvas(self, fig): - return self.canvas.__class__(fig) - - def trigger(self, *args): - NavigationToolbar2GTK3.configure_subplots( - self._make_classic_style_pseudo_toolbar(), None) - - class HelpGTK3(backend_tools.ToolHelpBase): def _normalize_shortcut(self, key): """ @@ -836,14 +707,6 @@ def trigger(self, *args, **kwargs): clipboard.set_image(pb) -# Define the file to use as the GTk icon -if sys.platform == 'win32': - icon_filename = 'matplotlib.png' -else: - icon_filename = 'matplotlib.svg' -window_icon = str(cbook._get_data_path('images', icon_filename)) - - def error_msg_gtk(msg, parent=None): if parent is not None: # find the toplevel Gtk.Window parent = parent.get_toplevel() @@ -868,18 +731,6 @@ def error_msg_gtk(msg, parent=None): @_Backend.export -class _BackendGTK3(_Backend): +class _BackendGTK3(_BackendGTK): FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 - - @staticmethod - def mainloop(): - global _application - if _application is None: - return - - try: - _application.run() # Quits when all added windows close. - finally: - # Running after quit is undefined, so create a new one next time. - _application = None diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py new file mode 100644 index 000000000000..3ffff77792db --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -0,0 +1,673 @@ +import functools +import io +import os +from pathlib import Path +import sys + +import matplotlib as mpl +from matplotlib import _api, backend_tools, cbook +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import ( + _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, + TimerBase, ToolContainerBase) +from matplotlib.backend_tools import Cursors +from matplotlib.figure import Figure +from matplotlib.widgets import SubplotTool + +try: + import gi +except ImportError as err: + raise ImportError("The GTK4 backends require PyGObject") from err + +try: + # :raises ValueError: If module/version is already loaded, already + # required, or unavailable. + gi.require_version("Gtk", "4.0") +except ValueError as e: + # in this case we want to re-raise as ImportError so the + # auto-backend selection logic correctly skips. + raise ImportError from e + +from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf +from ._backend_gtk import ( + _create_application, _shutdown_application, + backend_version, _BackendGTK, _NavigationToolbar2GTK, + TimerGTK as TimerGTK4, + ConfigureSubplotsGTK as ConfigureSubplotsGTK4, + RubberbandGTK as RubberbandGTK4, +) + + +def _mpl_to_gtk_cursor(mpl_cursor): + return _api.check_getitem({ + Cursors.MOVE: "move", + Cursors.HAND: "pointer", + Cursors.POINTER: "default", + Cursors.SELECT_REGION: "crosshair", + Cursors.WAIT: "wait", + Cursors.RESIZE_HORIZONTAL: "ew-resize", + Cursors.RESIZE_VERTICAL: "ns-resize", + }, cursor=mpl_cursor) + + +class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase): + required_interactive_framework = "gtk4" + _timer_cls = TimerGTK4 + + def __init__(self, figure=None): + FigureCanvasBase.__init__(self, figure) + GObject.GObject.__init__(self) + self.set_hexpand(True) + self.set_vexpand(True) + + self._idle_draw_id = 0 + self._lastCursor = None + self._rubberband_rect = None + + self.set_draw_func(self._draw_func) + self.connect('resize', self.resize_event) + + click = Gtk.GestureClick() + click.set_button(0) # All buttons. + click.connect('pressed', self.button_press_event) + click.connect('released', self.button_release_event) + self.add_controller(click) + + key = Gtk.EventControllerKey() + key.connect('key-pressed', self.key_press_event) + key.connect('key-released', self.key_release_event) + self.add_controller(key) + + motion = Gtk.EventControllerMotion() + motion.connect('motion', self.motion_notify_event) + motion.connect('enter', self.enter_notify_event) + motion.connect('leave', self.leave_notify_event) + self.add_controller(motion) + + scroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL) + scroll.connect('scroll', self.scroll_event) + self.add_controller(scroll) + + self.set_focusable(True) + + css = Gtk.CssProvider() + css.load_from_data(b".matplotlib-canvas { background-color: white; }") + style_ctx = self.get_style_context() + style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + style_ctx.add_class("matplotlib-canvas") + + def pick(self, mouseevent): + # GtkWidget defines pick in GTK4, so we need to override here to work + # with the base implementation we want. + FigureCanvasBase.pick(self, mouseevent) + + def destroy(self): + self.close_event() + + def set_cursor(self, cursor): + # docstring inherited + self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor)) + + def scroll_event(self, controller, dx, dy): + FigureCanvasBase.scroll_event(self, 0, 0, dy) + return True + + def button_press_event(self, controller, n_press, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.button_press_event(self, x, y, + controller.get_current_button()) + self.grab_focus() + + def button_release_event(self, controller, n_press, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.button_release_event(self, x, y, + controller.get_current_button()) + + def key_press_event(self, controller, keyval, keycode, state): + key = self._get_key(keyval, keycode, state) + FigureCanvasBase.key_press_event(self, key) + return True + + def key_release_event(self, controller, keyval, keycode, state): + key = self._get_key(keyval, keycode, state) + FigureCanvasBase.key_release_event(self, key) + return True + + def motion_notify_event(self, controller, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.motion_notify_event(self, x, y) + + def leave_notify_event(self, controller): + FigureCanvasBase.leave_notify_event(self) + + def enter_notify_event(self, controller, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.enter_notify_event(self, xy=(x, y)) + + def resize_event(self, area, width, height): + dpi = self.figure.dpi + self.figure.set_size_inches(width / dpi, height / dpi, forward=False) + FigureCanvasBase.resize_event(self) + self.draw_idle() + + def _get_key(self, keyval, keycode, state): + unikey = chr(Gdk.keyval_to_unicode(keyval)) + key = cbook._unikey_or_keysym_to_mplkey( + unikey, + Gdk.keyval_name(keyval)) + modifiers = [ + (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), + (Gdk.ModifierType.ALT_MASK, 'alt'), + (Gdk.ModifierType.SHIFT_MASK, 'shift'), + (Gdk.ModifierType.SUPER_MASK, 'super'), + ] + for key_mask, prefix in modifiers: + if state & key_mask: + if not (prefix == 'shift' and unikey.isprintable()): + key = f'{prefix}+{key}' + return key + + def _draw_rubberband(self, rect): + self._rubberband_rect = rect + # TODO: Only update the rubberband area. + self.queue_draw() + + def _draw_func(self, drawing_area, ctx, width, height): + self.on_draw_event(self, ctx) + self._post_draw(self, ctx) + + def _post_draw(self, widget, ctx): + if self._rubberband_rect is None: + return + + x0, y0, w, h = self._rubberband_rect + x1 = x0 + w + y1 = y0 + h + + # Draw the lines from x0, y0 towards x1, y1 so that the + # dashes don't "jump" when moving the zoom box. + ctx.move_to(x0, y0) + ctx.line_to(x0, y1) + ctx.move_to(x0, y0) + ctx.line_to(x1, y0) + ctx.move_to(x0, y1) + ctx.line_to(x1, y1) + ctx.move_to(x1, y0) + ctx.line_to(x1, y1) + + ctx.set_antialias(1) + ctx.set_line_width(1) + ctx.set_dash((3, 3), 0) + ctx.set_source_rgb(0, 0, 0) + ctx.stroke_preserve() + + ctx.set_dash((3, 3), 3) + ctx.set_source_rgb(1, 1, 1) + ctx.stroke() + + def on_draw_event(self, widget, ctx): + # to be overwritten by GTK4Agg or GTK4Cairo + pass + + def draw(self): + # docstring inherited + if self.is_drawable(): + self.queue_draw() + + def draw_idle(self): + # docstring inherited + if self._idle_draw_id != 0: + return + def idle_draw(*args): + try: + self.draw() + finally: + self._idle_draw_id = 0 + return False + self._idle_draw_id = GLib.idle_add(idle_draw) + + def flush_events(self): + # docstring inherited + context = GLib.MainContext.default() + while context.pending(): + context.iteration(True) + + +class FigureManagerGTK4(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : Gtk.Box + The toolbar + vbox : Gtk.VBox + The Gtk.VBox containing the canvas and toolbar + window : Gtk.Window + The Gtk.Window + + """ + def __init__(self, canvas, num): + app = _create_application() + self.window = Gtk.Window() + app.add_window(self.window) + super().__init__(canvas, num) + + self.vbox = Gtk.Box() + self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) + self.window.set_child(self.vbox) + + self.vbox.prepend(self.canvas) + # calculate size for window + w = int(self.canvas.figure.bbox.width) + h = int(self.canvas.figure.bbox.height) + + self.toolbar = self._get_toolbar() + + if self.toolmanager: + backend_tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + + if self.toolbar is not None: + sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) + sw.set_child(self.toolbar) + self.vbox.append(sw) + min_size, nat_size = self.toolbar.get_preferred_size() + h += nat_size.height + + self.window.set_default_size(w, h) + + self._destroying = False + self.window.connect("destroy", lambda *args: Gcf.destroy(self)) + self.window.connect("close-request", lambda *args: Gcf.destroy(self)) + if mpl.is_interactive(): + self.window.show() + self.canvas.draw_idle() + + self.canvas.grab_focus() + + def destroy(self, *args): + if self._destroying: + # Otherwise, this can be called twice when the user presses 'q', + # which calls Gcf.destroy(self), then this destroy(), then triggers + # Gcf.destroy(self) once again via + # `connect("destroy", lambda *args: Gcf.destroy(self))`. + return + self._destroying = True + self.window.destroy() + self.canvas.destroy() + + def show(self): + # show the figure window + self.window.show() + self.canvas.draw() + if mpl.rcParams['figure.raise_window']: + if self.window.get_surface(): + self.window.present() + else: + # If this is called by a callback early during init, + # self.window (a GtkWindow) may not have an associated + # low-level GdkSurface (self.window.get_surface()) yet, and + # present() would crash. + _api.warn_external("Cannot raise window yet to be setup") + + def full_screen_toggle(self): + if not self.window.is_fullscreen(): + self.window.fullscreen() + else: + self.window.unfullscreen() + + def _get_toolbar(self): + # must be inited after the window, drawingArea and figure + # attrs are set + if mpl.rcParams['toolbar'] == 'toolbar2': + toolbar = NavigationToolbar2GTK4(self.canvas, self.window) + elif mpl.rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarGTK4(self.toolmanager) + else: + toolbar = None + return toolbar + + def get_window_title(self): + return self.window.get_title() + + def set_window_title(self, title): + self.window.set_title(title) + + def resize(self, width, height): + """Set the canvas size in pixels.""" + if self.toolbar: + toolbar_size = self.toolbar.size_request() + height += toolbar_size.height + canvas_size = self.canvas.get_allocation() + if canvas_size.width == canvas_size.height == 1: + # A canvas size of (1, 1) cannot exist in most cases, because + # window decorations would prevent such a small window. This call + # must be before the window has been mapped and widgets have been + # sized, so just change the window's starting size. + self.window.set_default_size(width, height) + else: + self.window.resize(width, height) + + +class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box): + def __init__(self, canvas, window): + self.win = window + Gtk.Box.__init__(self) + + self.add_css_class('toolbar') + + self._gtk_ids = {} + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + self.append(Gtk.Separator()) + continue + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string( + str(cbook._get_data_path('images', + f'{image_file}-symbolic.svg')))) + self._gtk_ids[text] = button = ( + Gtk.ToggleButton() if callback in ['zoom', 'pan'] else + Gtk.Button()) + button.set_child(image) + button.add_css_class('flat') + button.add_css_class('image-button') + # Save the handler id, so that we can block it as needed. + button._signal_handler = button.connect( + 'clicked', getattr(self, callback)) + button.set_tooltip_text(tooltip_text) + self.append(button) + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self.message = Gtk.Label() + self.append(self.message) + + NavigationToolbar2.__init__(self, canvas) + + def save_figure(self, *args): + dialog = Gtk.FileChooserNative( + title='Save the figure', + transient_for=self.canvas.get_root(), + action=Gtk.FileChooserAction.SAVE, + modal=True) + self._save_dialog = dialog # Must keep a reference. + + ff = Gtk.FileFilter() + ff.set_name('All files') + ff.add_pattern('*') + dialog.add_filter(ff) + dialog.set_filter(ff) + + formats = [] + default_format = None + for i, (name, fmts) in enumerate( + self.canvas.get_supported_filetypes_grouped().items()): + ff = Gtk.FileFilter() + ff.set_name(name) + for fmt in fmts: + ff.add_pattern(f'*.{fmt}') + dialog.add_filter(ff) + formats.append(name) + if self.canvas.get_default_filetype() in fmts: + default_format = i + # Setting the choice doesn't always work, so make sure the default + # format is first. + formats = [formats[default_format], *formats[:default_format], + *formats[default_format+1:]] + dialog.add_choice('format', 'File format', formats, formats) + dialog.set_choice('format', formats[default_format]) + + dialog.set_current_folder(Gio.File.new_for_path( + os.path.expanduser(mpl.rcParams['savefig.directory']))) + dialog.set_current_name(self.canvas.get_default_filename()) + + @functools.partial(dialog.connect, 'response') + def on_response(dialog, response): + file = dialog.get_file() + fmt = dialog.get_choice('format') + fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0] + dialog.destroy() + self._save_dialog = None + if response != Gtk.ResponseType.ACCEPT: + return + # Save dir for next time, unless empty str (which means use cwd). + if mpl.rcParams['savefig.directory']: + parent = file.get_parent() + mpl.rcParams['savefig.directory'] = parent.get_path() + try: + self.canvas.figure.savefig(file.get_path(), format=fmt) + except Exception as e: + msg = Gtk.MessageDialog( + transient_for=self.canvas.get_root(), + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, modal=True, + text=str(e)) + msg.show() + + dialog.show() + + +class ToolbarGTK4(ToolContainerBase, Gtk.Box): + _icon_extension = '-symbolic.svg' + + def __init__(self, toolmanager): + ToolContainerBase.__init__(self, toolmanager) + Gtk.Box.__init__(self) + self.set_property('orientation', Gtk.Orientation.HORIZONTAL) + + # Tool items are created later, but must appear before the message. + self._tool_box = Gtk.Box() + self.append(self._tool_box) + self._groups = {} + self._toolitems = {} + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self._message = Gtk.Label() + self.append(self._message) + + def add_toolitem(self, name, group, position, image_file, description, + toggle): + if toggle: + button = Gtk.ToggleButton() + else: + button = Gtk.Button() + button.set_label(name) + button.add_css_class('flat') + + if image_file is not None: + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string(image_file)) + button.set_child(image) + button.add_css_class('image-button') + + if position is None: + position = -1 + + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + self._toolitems.setdefault(name, []) + self._toolitems[name].append((button, signal)) + + def _find_child_at_position(self, group, position): + children = [None] + child = self._groups[group].get_first_child() + while child is not None: + children.append(child) + child = child.get_next_sibling() + return children[position] + + def _add_button(self, button, group, position): + if group not in self._groups: + if self._groups: + self._add_separator() + group_box = Gtk.Box() + self._tool_box.append(group_box) + self._groups[group] = group_box + self._groups[group].insert_child_after( + button, self._find_child_at_position(group, position)) + + def _call_tool(self, btn, name): + self.trigger_tool(name) + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for toolitem, signal in self._toolitems[name]: + toolitem.handler_block(signal) + toolitem.set_active(toggled) + toolitem.handler_unblock(signal) + + def remove_toolitem(self, name): + if name not in self._toolitems: + self.toolmanager.message_event(f'{name} not in toolbar', self) + return + + for group in self._groups: + for toolitem, _signal in self._toolitems[name]: + if toolitem in self._groups[group]: + self._groups[group].remove(toolitem) + del self._toolitems[name] + + def _add_separator(self): + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + self._tool_box.append(sep) + + def set_message(self, s): + self._message.set_label(s) + + +class SaveFigureGTK4(backend_tools.SaveFigureBase): + def trigger(self, *args, **kwargs): + + class PseudoToolbar: + canvas = self.figure.canvas + + return NavigationToolbar2GTK4.save_figure(PseudoToolbar()) + + +class HelpGTK4(backend_tools.ToolHelpBase): + def _normalize_shortcut(self, key): + """ + Convert Matplotlib key presses to GTK+ accelerator identifiers. + + Related to `FigureCanvasGTK4._get_key`. + """ + special = { + 'backspace': 'BackSpace', + 'pagedown': 'Page_Down', + 'pageup': 'Page_Up', + 'scroll_lock': 'Scroll_Lock', + } + + parts = key.split('+') + mods = ['<' + mod + '>' for mod in parts[:-1]] + key = parts[-1] + + if key in special: + key = special[key] + elif len(key) > 1: + key = key.capitalize() + elif key.isupper(): + mods += [''] + + return ''.join(mods) + key + + def _is_valid_shortcut(self, key): + """ + Check for a valid shortcut to be displayed. + + - GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`). + - The shortcut window only shows keyboard shortcuts, not mouse buttons. + """ + return 'cmd+' not in key and not key.startswith('MouseButton.') + + def trigger(self, *args): + section = Gtk.ShortcutsSection() + + for name, tool in sorted(self.toolmanager.tools.items()): + if not tool.description: + continue + + # Putting everything in a separate group allows GTK to + # automatically split them into separate columns/pages, which is + # useful because we have lots of shortcuts, some with many keys + # that are very wide. + group = Gtk.ShortcutsGroup() + section.append(group) + # A hack to remove the title since we have no group naming. + child = group.get_first_child() + while child is not None: + child.set_visible(False) + child = child.get_next_sibling() + + shortcut = Gtk.ShortcutsShortcut( + accelerator=' '.join( + self._normalize_shortcut(key) + for key in self.toolmanager.get_tool_keymap(name) + if self._is_valid_shortcut(key)), + title=tool.name, + subtitle=tool.description) + group.append(shortcut) + + window = Gtk.ShortcutsWindow( + title='Help', + modal=True, + transient_for=self._figure.canvas.get_root()) + window.set_child(section) + + window.show() + + +class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase): + def trigger(self, *args, **kwargs): + with io.BytesIO() as f: + self.canvas.print_rgba(f) + w, h = self.canvas.get_width_height() + pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(), + GdkPixbuf.Colorspace.RGB, True, + 8, w, h, w*4) + clipboard = self.canvas.get_clipboard() + clipboard.set(pb) + + +backend_tools.ToolSaveFigure = SaveFigureGTK4 +backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK4 +backend_tools.ToolRubberband = RubberbandGTK4 +backend_tools.ToolHelp = HelpGTK4 +backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK4 + +Toolbar = ToolbarGTK4 + + +@_Backend.export +class _BackendGTK4(_BackendGTK): + FigureCanvas = FigureCanvasGTK4 + FigureManager = FigureManagerGTK4 diff --git a/lib/matplotlib/backends/backend_gtk4agg.py b/lib/matplotlib/backends/backend_gtk4agg.py new file mode 100644 index 000000000000..b3439dc109cd --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4agg.py @@ -0,0 +1,80 @@ +import numpy as np + +from .. import cbook +try: + from . import backend_cairo +except ImportError as e: + raise ImportError('backend Gtk4Agg requires cairo') from e +from . import backend_agg, backend_gtk4 +from .backend_cairo import cairo +from .backend_gtk4 import Gtk, _BackendGTK4 +from matplotlib import transforms + + +class FigureCanvasGTK4Agg(backend_gtk4.FigureCanvasGTK4, + backend_agg.FigureCanvasAgg): + def __init__(self, figure): + backend_gtk4.FigureCanvasGTK4.__init__(self, figure) + self._bbox_queue = [] + + def on_draw_event(self, widget, ctx): + allocation = self.get_allocation() + w, h = allocation.width, allocation.height + + if not len(self._bbox_queue): + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + bbox_queue = [transforms.Bbox([[0, 0], [w, h]])] + else: + bbox_queue = self._bbox_queue + + ctx = backend_cairo._to_context(ctx) + + for bbox in bbox_queue: + x = int(bbox.x0) + y = h - int(bbox.y1) + width = int(bbox.x1) - int(bbox.x0) + height = int(bbox.y1) - int(bbox.y0) + + buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( + np.asarray(self.copy_from_bbox(bbox))) + image = cairo.ImageSurface.create_for_data( + buf.ravel().data, cairo.FORMAT_ARGB32, width, height) + ctx.set_source_surface(image, x, y) + ctx.paint() + + if len(self._bbox_queue): + self._bbox_queue = [] + + return False + + def blit(self, bbox=None): + # If bbox is None, blit the entire canvas to gtk. Otherwise + # blit only the area defined by the bbox. + if bbox is None: + bbox = self.figure.bbox + + allocation = self.get_allocation() + x = int(bbox.x0) + y = allocation.height - int(bbox.y1) + width = int(bbox.x1) - int(bbox.x0) + height = int(bbox.y1) - int(bbox.y0) + + self._bbox_queue.append(bbox) + self.queue_draw_area(x, y, width, height) + + def draw(self): + backend_agg.FigureCanvasAgg.draw(self) + super().draw() + + +class FigureManagerGTK4Agg(backend_gtk4.FigureManagerGTK4): + pass + + +@_BackendGTK4.export +class _BackendGTK4Agg(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Agg + FigureManager = FigureManagerGTK4Agg diff --git a/lib/matplotlib/backends/backend_gtk4cairo.py b/lib/matplotlib/backends/backend_gtk4cairo.py new file mode 100644 index 000000000000..391a1a372856 --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4cairo.py @@ -0,0 +1,35 @@ +from contextlib import nullcontext + +from . import backend_cairo, backend_gtk4 +from .backend_gtk4 import Gtk, _BackendGTK4 + + +class RendererGTK4Cairo(backend_cairo.RendererCairo): + def set_context(self, ctx): + self.gc.ctx = backend_cairo._to_context(ctx) + + +class FigureCanvasGTK4Cairo(backend_gtk4.FigureCanvasGTK4, + backend_cairo.FigureCanvasCairo): + + def __init__(self, figure): + super().__init__(figure) + self._renderer = RendererGTK4Cairo(self.figure.dpi) + + def on_draw_event(self, widget, ctx): + with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar + else nullcontext()): + self._renderer.set_context(ctx) + allocation = self.get_allocation() + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + self._renderer.set_width_height( + allocation.width, allocation.height) + self.figure.draw(self._renderer) + + +@_BackendGTK4.export +class _BackendGTK4Cairo(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Cairo diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 9980d49af85b..bd0c370f1cba 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1063,12 +1063,12 @@ def createType1Descriptor(self, t1font, fontfile): return fontdescObject - def _get_xobject_symbol_name(self, filename, symbol_name): + def _get_xobject_glyph_name(self, filename, glyph_name): Fx = self.fontName(filename) return "-".join([ Fx.name.decode(), os.path.splitext(os.path.basename(filename))[0], - symbol_name]) + glyph_name]) _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin @@ -1204,7 +1204,7 @@ def get_char_width(charcode): # Send the glyphs with ccode > 255 to the XObject dictionary, # and the others to the font itself if charname in multi_byte_chars: - name = self._get_xobject_symbol_name(filename, charname) + name = self._get_xobject_glyph_name(filename, charname) self.multi_byte_charprocs[name] = charprocObject else: charprocs[charname] = charprocObject @@ -1347,7 +1347,7 @@ def embedTTFType42(font, characters, descriptor): self.currentstream.write(stream) self.endStream() - name = self._get_xobject_symbol_name(filename, charname) + name = self._get_xobject_glyph_name(filename, charname) self.multi_byte_charprocs[name] = charprocObject # CIDToGIDMap stream @@ -2417,8 +2417,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): """Draw a multibyte character from a Type 3 font as an XObject.""" - symbol_name = font.get_glyph_name(glyph_idx) - name = self.file._get_xobject_symbol_name(font.fname, symbol_name) + glyph_name = font.get_glyph_name(glyph_idx) + name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, @@ -2793,7 +2793,7 @@ def print_pdf(self, filename, *, file.close() def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3f1cb7b172eb..2fa8c3251b12 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -600,6 +600,30 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): r"{\pgfqpoint{%fin}{%fin}}" % coords) + # apply pgf decorators + sketch_params = gc.get_sketch_params() if gc else None + if sketch_params is not None: + # Only "length" directly maps to "segment length" in PGF's API. + # PGF uses "amplitude" to pass the combined deviation in both x- + # and y-direction, while matplotlib only varies the length of the + # wiggle along the line ("randomness" and "length" parameters) + # and has a separate "scale" argument for the amplitude. + # -> Use "randomness" as PRNG seed to allow the user to force the + # same shape on multiple sketched lines + scale, length, randomness = sketch_params + if scale is not None: + # make matplotlib and PGF rendering visually similar + length *= 0.5 + scale *= 2 + # PGF guarantees that repeated loading is a no-op + writeln(self.fh, r"\usepgfmodule{decorations}") + writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") + writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " + f"segment length = {(length * f):f}in, " + f"amplitude = {(scale * f):f}in}}") + writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") + writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") + def _pgf_path_draw(self, stroke=True, fill=False): actions = [] if stroke: @@ -883,7 +907,7 @@ def get_renderer(self): return RendererPgf(self.figure, None) def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index f13e114a815b..c44f89c638d9 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -701,12 +701,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - symbol_name = ( + glyph_name = ( font.get_name_char(chr(num)) if isinstance(font, AFM) else font.get_glyph_name(font.get_char_index(num))) self._pswriter.write( f"{ox:f} {oy:f} moveto\n" - f"/{symbol_name} glyphshow\n") + f"/{glyph_name} glyphshow\n") for ox, oy, w, h in rects: self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n") self._pswriter.write("grestore\n") @@ -1119,7 +1119,7 @@ def _print_figure_tex( _move_path_to_path_or_stream(tmpfile, outfile) def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 5b74176befff..e8a137d6a565 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -333,7 +333,8 @@ def keyReleaseEvent(self, event): def resizeEvent(self, event): frame = sys._getframe() - if frame.f_code is frame.f_back.f_code: # Prevent PyQt6 recursion. + # Prevent PyQt6 recursion, but sometimes frame.f_back is None + if frame.f_code is getattr(frame.f_back, 'f_code', None): return w = event.size().width() * self.device_pixel_ratio h = event.size().height() * self.device_pixel_ratio diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 904cca7bf313..e4de85905ca7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1343,7 +1343,7 @@ def get_default_filetype(self): return 'svg' def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 16b152a51689..da78f69d9e8e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -592,8 +592,8 @@ def _get_imagesave_wildcards(self): @_api.delete_parameter("3.4", "origin") def gui_repaint(self, drawDC=None, origin='WX'): """ - Performs update of the displayed image on the GUI canvas, using the - supplied wx.PaintDC device context. + Update the displayed image on the GUI canvas, using the supplied + wx.PaintDC device context. The 'WXAgg' backend sets origin accordingly. """ diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 109b9ea69cc9..6d181c43107d 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -50,8 +50,8 @@ def _get_running_interactive_framework(): Returns ------- Optional[str] - One of the following values: "qt", "gtk3", "wx", "tk", "macosx", - "headless", ``None``. + One of the following values: "qt", "gtk3", "gtk4", "wx", "tk", + "macosx", "headless", ``None``. """ # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as # entries can also have been explicitly set to None. @@ -64,8 +64,13 @@ def _get_running_interactive_framework(): if QtWidgets and QtWidgets.QApplication.instance(): return "qt" Gtk = sys.modules.get("gi.repository.Gtk") - if Gtk and Gtk.main_level(): - return "gtk3" + if Gtk: + if Gtk.MAJOR_VERSION == 4: + from gi.repository import GLib + if GLib.main_depth(): + return "gtk4" + if Gtk.MAJOR_VERSION == 3 and Gtk.main_level(): + return "gtk3" wx = sys.modules.get("wx") if wx and wx.GetApp(): return "wx" diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 0af2f0f327d9..76c1c5d4f7f2 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -337,7 +337,7 @@ def __init__(self, norm=None, cmap=None): 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._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. @@ -496,6 +496,8 @@ def set_clim(self, vmin=None, vmax=None): .. 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 @@ -505,7 +507,6 @@ def set_clim(self, vmin=None, vmax=None): self.norm.vmin = colors._sanitize_extrema(vmin) if vmax is not None: self.norm.vmax = colors._sanitize_extrema(vmax) - self.changed() def get_alpha(self): """ @@ -531,6 +532,30 @@ def set_cmap(self, 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, None), norm=norm) + if norm is None: + norm = colors.Normalize() + + 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. @@ -545,13 +570,7 @@ def set_norm(self, norm): the norm of the mappable will reset the norm, locator, and formatters on the colorbar to default. """ - _api.check_isinstance((colors.Normalize, None), norm=norm) - in_init = self.norm is None - if norm is None: - norm = colors.Normalize() self.norm = norm - if not in_init: - self.changed() # Things are not set up properly yet. def autoscale(self): """ @@ -560,8 +579,9 @@ def autoscale(self): """ 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) - self.changed() def autoscale_None(self): """ @@ -570,8 +590,9 @@ def autoscale_None(self): """ 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) - self.changed() def changed(self): """ diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 459b14f6c5a7..d826649af167 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -471,6 +471,7 @@ def __init__(self, ax, mappable=None, *, cmap=None, self.ax.add_collection(self.dividers) self.locator = None + self.minorlocator = None self.formatter = None self.__scale = None # linear, log10 for now. Hopefully more? @@ -1096,7 +1097,7 @@ def _mesh(self): # vmax of the colorbar, not the norm. This allows the situation # where the colormap has a narrower range than the colorbar, to # accommodate extra contours: - norm = copy.copy(self.norm) + norm = copy.deepcopy(self.norm) norm.vmin = self.vmin norm.vmax = self.vmax x = np.array([0.0, 1.0]) @@ -1197,8 +1198,9 @@ def _proportional_y(self): a proportional colorbar, plus extension lengths if required: """ if isinstance(self.norm, colors.BoundaryNorm): - y = (self._boundaries - self._boundaries[0]) - y = y / (self._boundaries[-1] - self._boundaries[0]) + y = (self._boundaries - self._boundaries[self._inside][0]) + y = y / (self._boundaries[self._inside][-1] - + self._boundaries[self._inside][0]) # need yscaled the same as the axes scale to get # the extend lengths. if self.spacing == 'uniform': diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f75e069d3083..190dfb448901 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1123,10 +1123,50 @@ def __init__(self, vmin=None, vmax=None, clip=False): ----- Returns 0 if ``vmin == vmax``. """ - self.vmin = _sanitize_extrema(vmin) - self.vmax = _sanitize_extrema(vmax) - self.clip = clip - self._scale = None # will default to LinearScale for colorbar + self._vmin = _sanitize_extrema(vmin) + self._vmax = _sanitize_extrema(vmax) + self._clip = clip + self._scale = None + self.callbacks = cbook.CallbackRegistry() + + @property + def vmin(self): + return self._vmin + + @vmin.setter + def vmin(self, value): + value = _sanitize_extrema(value) + if value != self._vmin: + self._vmin = value + self._changed() + + @property + def vmax(self): + return self._vmax + + @vmax.setter + def vmax(self, value): + value = _sanitize_extrema(value) + if value != self._vmax: + self._vmax = value + self._changed() + + @property + def clip(self): + return self._clip + + @clip.setter + def clip(self, value): + if value != self._clip: + self._clip = value + self._changed() + + def _changed(self): + """ + Call this whenever the norm is changed to notify all the + callback listeners to the 'changed' signal. + """ + self.callbacks.process('changed') @staticmethod def process_value(value): @@ -1273,7 +1313,7 @@ def __init__(self, vcenter, vmin=None, vmax=None): """ super().__init__(vmin=vmin, vmax=vmax) - self.vcenter = vcenter + self._vcenter = vcenter if vcenter is not None and vmax is not None and vcenter >= vmax: raise ValueError('vmin, vcenter, and vmax must be in ' 'ascending order') @@ -1281,6 +1321,16 @@ def __init__(self, vcenter, vmin=None, vmax=None): raise ValueError('vmin, vcenter, and vmax must be in ' 'ascending order') + @property + def vcenter(self): + return self._vcenter + + @vcenter.setter + def vcenter(self, value): + if value != self._vcenter: + self._vcenter = value + self._changed() + def autoscale_None(self, A): """ Get vmin and vmax, and then clip at vcenter @@ -1387,7 +1437,9 @@ def vcenter(self): @vcenter.setter def vcenter(self, vcenter): - self._vcenter = vcenter + if vcenter != self._vcenter: + self._vcenter = vcenter + self._changed() if self.vmax is not None: # recompute halfrange assuming vmin and vmax represent # min and max of data @@ -2324,7 +2376,7 @@ def blend_soft_light(self, rgb, intensity): def blend_overlay(self, rgb, intensity): """ - Combines an rgb image with an intensity map using "overlay" blending. + Combine an rgb image with an intensity map using "overlay" blending. Parameters ---------- diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 7dec80943993..4d124ce8c57c 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1090,6 +1090,15 @@ def _make_paths(self, segs, kinds): in zip(segs, kinds)] def changed(self): + if not hasattr(self, "cvalues"): + # Just return after calling the super() changed function + cm.ScalarMappable.changed(self) + return + # Force an autoscale immediately because self.to_rgba() calls + # autoscale_None() internally with the data passed to it, + # so if vmin/vmax are not set yet, this would override them with + # content from *cvalues* rather than levels like we want + self.norm.autoscale_None(self.levels) tcolors = [(tuple(rgba),) for rgba in self.to_rgba(self.cvalues, alpha=self.alpha)] self.tcolors = tcolors diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 67aff6270815..7ec320ffab33 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -21,8 +21,8 @@ .. seealso:: - :doc:`/gallery/text_labels_and_annotations/date` - - :doc:`/gallery/ticks_and_spines/date_concise_formatter` - - :doc:`/gallery/ticks_and_spines/date_demo_convert` + - :doc:`/gallery/ticks/date_concise_formatter` + - :doc:`/gallery/ticks/date_demo_convert` .. _date-format: @@ -38,7 +38,7 @@ 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 -:doc:`/gallery/ticks_and_spines/date_precision_and_epochs` for a discussion. +:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -144,7 +144,7 @@ * `RRuleLocator`: Locate using a `matplotlib.dates.rrulewrapper`. `.rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` which allow almost arbitrary date tick specifications. See :doc:`rrule example - `. + `. * `AutoDateLocator`: On autoscale, this class picks the best `DateLocator` (e.g., `RRuleLocator`) to set the view limits and the tick locations. If @@ -271,7 +271,7 @@ def set_epoch(epoch): `~.dates.set_epoch` must be called before any dates are converted (i.e. near the import section) or a RuntimeError will be raised. - See also :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See also :doc:`/gallery/ticks/date_precision_and_epochs`. Parameters ---------- @@ -683,7 +683,7 @@ class ConciseDateFormatter(ticker.Formatter): Examples -------- - See :doc:`/gallery/ticks_and_spines/date_concise_formatter` + See :doc:`/gallery/ticks/date_concise_formatter` .. plot:: @@ -1659,7 +1659,7 @@ class MicrosecondLocator(DateLocator): If you really must use datetime.datetime() or similar and still need microsecond precision, change the time origin via `.dates.set_epoch` to something closer to the dates being plotted. - See :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See :doc:`/gallery/ticks/date_precision_and_epochs`. """ def __init__(self, interval=1, tz=None): diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 117c60aa786d..3207a01de8be 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -84,8 +84,6 @@ def _arg(nbytes, signed, dvi, _): def _arg_slen(dvi, delta): """ - Signed, length *delta* - Read *delta* bytes, returning None if *delta* is zero, and the bytes interpreted as a signed integer otherwise. """ @@ -96,26 +94,20 @@ def _arg_slen(dvi, delta): def _arg_slen1(dvi, delta): """ - Signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as signed. """ - return dvi._arg(delta+1, True) + return dvi._arg(delta + 1, True) def _arg_ulen1(dvi, delta): """ - Unsigned length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned. """ - return dvi._arg(delta+1, False) + return dvi._arg(delta + 1, False) def _arg_olen1(dvi, delta): """ - Optionally signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned integer for 0<=*delta*<3 and signed if *delta*==3. """ @@ -139,30 +131,30 @@ def _dispatch(table, min, max=None, state=None, args=('raw',)): matches *state* if not None, reads arguments from the file according to *args*. - *table* - the dispatch table to be filled in - - *min* - minimum opcode for calling this function - - *max* - maximum opcode for calling this function, None if only *min* is allowed - - *state* - state of the Dvi object in which these opcodes are allowed - - *args* - sequence of argument specifications: - - ``'raw'``: opcode minus minimum - ``'u1'``: read one unsigned byte - ``'u4'``: read four bytes, treat as an unsigned number - ``'s4'``: read four bytes, treat as a signed number - ``'slen'``: read (opcode - minimum) bytes, treat as signed - ``'slen1'``: read (opcode - minimum + 1) bytes, treat as signed - ``'ulen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - ``'olen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - if under four bytes, signed if four bytes + Parameters + ---------- + table : dict[int, callable] + The dispatch table to be filled in. + + min, max : int + Range of opcodes that calls the registered function; *max* defaults to + *min*. + + state : _dvistate, optional + State of the Dvi object in which these opcodes are allowed. + + args : list[str], default: ['raw'] + Sequence of argument specifications: + + - 'raw': opcode minus minimum + - 'u1': read one unsigned byte + - 'u4': read four bytes, treat as an unsigned number + - 's4': read four bytes, treat as a signed number + - 'slen': read (opcode - minimum) bytes, treat as signed + - 'slen1': read (opcode - minimum + 1) bytes, treat as signed + - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned + - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned + if under four bytes, signed if four bytes """ def decorate(method): get_args = [_arg_mapping[x] for x in args] @@ -185,6 +177,7 @@ def wrapper(self, byte): class Dvi: """ A reader for a dvi ("device-independent") file, as produced by TeX. + The current implementation can only iterate through pages in order, and does not even attempt to verify the postamble. @@ -956,8 +949,9 @@ def _parse_and_cache_line(self, line): def _parse_enc(path): r""" - Parses a \*.enc file referenced from a psfonts.map style file. - The format this class understands is a very limited subset of PostScript. + Parse a \*.enc file referenced from a psfonts.map style file. + + The format supported by this function is a tiny subset of PostScript. Parameters ---------- diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index abe8ec694922..dbd879521f25 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2808,7 +2808,7 @@ def draw(self, renderer): self.canvas.draw_event(renderer) - def draw_no_output(self): + def draw_without_rendering(self): """ Draw the figure with no output. Useful to get the final size of artists that require a draw before their size is known (e.g. text). diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 752048d64a59..74b35d41797d 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -490,11 +490,19 @@ def __init__(self, nrows, ncols, wspace=None, hspace=None, height_ratios=None, width_ratios=None): """ - The number of rows and number of columns of the grid need to - be set. An instance of SubplotSpec is also needed to be set - from which the layout parameters will be inherited. The wspace - and hspace of the layout can be optionally specified or the - default values (from the figure or rcParams) will be used. + Parameters + ---------- + nrows, ncols : int + Number of rows and number of columns of the grid. + subplot_spec : SubplotSpec + Spec from which the layout parameters are inherited. + wspace, hspace : float, optional + See `GridSpec` for more details. If not specified default values + (from the figure or rcParams) are used. + height_ratios : array-like of length *nrows*, optional + See `GridSpecBase` for details. + width_ratios : array-like of length *ncols*, optional + See `GridSpecBase` for details. """ self._wspace = wspace self._hspace = hspace diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index ca5b7da5f808..2036bf7e17c9 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -537,11 +537,14 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, 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 - with cbook._setattr_cm(self.norm, - vmin=s_vmin, - vmax=s_vmax, - ): - output = self.norm(resampled_masked) + # Block the norm from sending an update signal during the + # temporary vmin/vmax change + with self.norm.callbacks.blocked(): + with cbook._setattr_cm(self.norm, + vmin=s_vmin, + vmax=s_vmax, + ): + output = self.norm(resampled_masked) else: if A.ndim == 2: # _interpolation_stage == 'rgba' self.norm.autoscale_None(A) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index a6fca87c2a45..3270f0b18011 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1197,7 +1197,10 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): elif len(args) == 0: handles, labels = _get_legend_handles_labels(axs, handlers) if not handles: - log.warning('No handles with labels found to put in legend.') + log.warning( + "No artists with labels found to put in legend. Note that " + "artists whose label start with an underscore are ignored " + "when legend() is called with no argument.") # One argument. User defined labels - automatic handle detection. elif len(args) == 1: diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index b289e26cc1f5..d4876ddc5c6b 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -214,19 +214,6 @@ class HandlerLine2DCompound(HandlerNpoints): a line-only with a marker-only artist. May be deprecated in the future. """ - def __init__(self, marker_pad=0.3, numpoints=None, **kwargs): - """ - Parameters - ---------- - marker_pad : float - Padding between points in legend entry. - numpoints : int - Number of points to show in legend entry. - **kwargs - Keyword arguments forwarded to `.HandlerNpoints`. - """ - super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kwargs) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): @@ -286,20 +273,6 @@ class HandlerLine2D(HandlerNpoints): artist for the line and another for the marker(s). """ - def __init__(self, marker_pad=0.3, numpoints=None, **kw): - """ - Parameters - ---------- - marker_pad : float - Padding between points in legend entry. - numpoints : int - Number of points to show in legend entry. - **kwargs - Keyword arguments forwarded to `.HandlerNpoints`. - """ - HandlerNpoints.__init__(self, marker_pad=marker_pad, - numpoints=numpoints, **kw) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 8f4ddaa5c6ea..9a727f9a8107 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -601,7 +601,7 @@ def get_markevery(self): def set_picker(self, p): """ - Sets the event picker details for the line. + Set the event picker details for the line. Parameters ---------- @@ -688,7 +688,7 @@ def recache(self, always=False): def _transform_path(self, subslice=None): """ - Puts a TransformedPath instance at self._transformed_path; + Put a TransformedPath instance at self._transformed_path; all invalidation of the transform is then handled by the TransformedPath instance. """ diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 656aeb64dc0d..0ce21c5ba200 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -24,19 +24,27 @@ import numpy as np from PIL import Image -from matplotlib import _api, colors as mcolors, rcParams, _mathtext +from matplotlib import ( + _api, colors as mcolors, rcParams, _mathtext, _mathtext_data) from matplotlib.ft2font import FT2Image, LOAD_NO_HINTING from matplotlib.font_manager import FontProperties -# Backcompat imports, all are deprecated as of 3.4. -from matplotlib._mathtext import ( # noqa: F401 - SHRINK_FACTOR, GROW_FACTOR, NUM_SIZE_LEVELS) -from matplotlib._mathtext_data import ( # noqa: F401 - latex_to_bakoma, latex_to_cmex, latex_to_standard, stix_virtual_fonts, - tex2uni) _log = logging.getLogger(__name__) +@_api.caching_module_getattr +class __getattr__: + locals().update({ + name: _api.deprecated("3.4")( + property(lambda self, _mod=mod, _name=name: getattr(_mod, _name))) + for mod, names in [ + (_mathtext, ["SHRINK_FACTOR", "GROW_FACTOR", "NUM_SIZE_LEVELS"]), + (_mathtext_data, [ + "latex_to_bakoma", "latex_to_cmex", "latex_to_standard", + "stix_virtual_fonts", "tex2uni"])] + for name in names}) + + get_unicode_index = _mathtext.get_unicode_index get_unicode_index.__module__ = __name__ @@ -580,18 +588,12 @@ def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None): format is determined as for `.Figure.savefig`. """ from matplotlib import figure - # backend_agg supports all of the core output formats - from matplotlib.backends import backend_agg - - if prop is None: - prop = FontProperties() parser = MathTextParser('path') width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) fig = figure.Figure(figsize=(width / 72.0, height / 72.0)) fig.text(0, depth/height, s, fontproperties=prop) - backend_agg.FigureCanvasAgg(fig) fig.savefig(filename_or_obj, dpi=dpi, format=format) return depth diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 19e89e3cdd5e..106d881ce88c 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -71,9 +71,9 @@ ## *************************************************************************** ## The default backend. If you omit this parameter, the first working ## backend from the following list is used: -## MacOSX QtAgg Gtk3Agg TkAgg WxAgg Agg +## MacOSX QtAgg Gtk4Agg Gtk3Agg TkAgg WxAgg Agg ## Other choices include: -## QtCairo GTK3Cairo TkCairo WxCairo Cairo +## QtCairo GTK4Cairo GTK3Cairo TkCairo WxCairo Cairo ## Qt5Agg Qt5Cairo Wx # deprecated. ## PS PDF SVG Template ## You can also deploy your own backend outside of Matplotlib by referring to diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index c09250123fb9..4280d55eeacd 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -162,7 +162,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, @classmethod def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): """ - Creates a Path instance without the expense of calling the constructor. + Create a Path instance without the expense of calling the constructor. Parameters ---------- diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 201255da848c..b222466dda45 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -212,6 +212,7 @@ def switch_backend(newbackend): current_framework = cbook._get_running_interactive_framework() mapping = {'qt': 'qtagg', 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', 'wx': 'wxagg', 'tk': 'tkagg', 'macosx': 'macosx', @@ -222,7 +223,8 @@ def switch_backend(newbackend): candidates = [best_guess] else: candidates = [] - candidates += ["macosx", "qtagg", "gtk3agg", "tkagg", "wxagg"] + candidates += [ + "macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"] # Don't try to fallback on the cairo-based backends as they each have # an additional dependency (pycairo) over the agg-based backend, and diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 2c3c88e2fa66..a8a54c10dac6 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -35,7 +35,7 @@ # The capitalized forms are needed for ipython at present; this may # change for later versions. interactive_bk = [ - 'GTK3Agg', 'GTK3Cairo', + 'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg', 'QtCairo', 'Qt5Agg', 'Qt5Cairo', diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 6de221476490..d6bceb6c9915 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -26,7 +26,9 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, Parameters ---------- x, y : 1D/2D arrays - Evenly spaced strictly increasing arrays to make a grid. + Evenly spaced strictly increasing arrays to make a grid. If 2D, all + rows of *x* must be equal and all columns of *y* must be equal; i.e., + they must be as if generated by ``np.meshgrid(x_1d, y_1d)``. u, v : 2D arrays *x* and *y*-velocities. The number of rows and columns must match the length of *y* and *x*, respectively. diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index 49d5cb7175f9..3c3a4b6273bc 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -2,15 +2,16 @@ ======================== Widget testing utilities ======================== -Functions that are useful for testing widgets. -See also matplotlib.tests.test_widgets + +See also :mod:`matplotlib.tests.test_widgets`. """ + import matplotlib.pyplot as plt from unittest import mock def get_ax(): - """Creates plot and returns its axes""" + """Create a plot and return its axes.""" fig, ax = plt.subplots(1, 1) ax.plot([0, 200], [0, 200]) ax.set_aspect(1.0) diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png b/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png new file mode 100644 index 000000000000..a1f9745230ba Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 509910bdeec1..32b0c202478b 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4793,7 +4793,7 @@ def test_reset_ticks(fig_test, fig_ref): labelsize=14, labelcolor='C1', labelrotation=45, grid_color='C2', grid_alpha=0.8, grid_linewidth=3, grid_linestyle='--') - fig.draw_no_output() + fig.draw_without_rendering() # After we've changed any setting on ticks, reset_ticks will mean # re-creating them from scratch. This *should* appear the same as not diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index a463c96e61fc..9b5b0b28ee3f 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -337,3 +337,30 @@ def test_minus_signs_with_tex(fig_test, fig_ref, texsystem): mpl.rcParams["pgf.texsystem"] = texsystem fig_test.text(.5, .5, "$-1$") fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$") + + +@pytest.mark.backend("pgf") +def test_sketch_params(): + fig, ax = plt.subplots(figsize=(3, 3)) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_frame_on(False) + handle, = ax.plot([0, 1]) + handle.set_sketch_params(scale=5, length=30, randomness=42) + + with BytesIO() as fd: + fig.savefig(fd, format='pgf') + buf = fd.getvalue().decode() + + baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% +\pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}% +\usepgfmodule{decorations}% +\usepgflibrary{decorations.pathmorphing}% +\pgfkeys{/pgf/decoration/.cd, """ \ + r"""segment length = 0.150000in, amplitude = 0.100000in}% +\pgfmathsetseed{42}% +\pgfdecoratecurrentpath{random steps}% +\pgfusepath{stroke}%""" + # \pgfdecoratecurrentpath must be after the path definition and before the + # path is used (\pgfusepath) + assert baseline in buf diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index a1f27fea577a..bb17e5fdaf82 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -29,8 +29,8 @@ def _get_testable_interactive_backends(): *[([qt_api, "cairocffi"], {"MPLBACKEND": "qtcairo", "QT_API": qt_api}) for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]], - (["cairo", "gi"], {"MPLBACKEND": "gtk3agg"}), - (["cairo", "gi"], {"MPLBACKEND": "gtk3cairo"}), + *[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"}) + for version in [3, 4] for renderer in ["agg", "cairo"]], (["tkinter"], {"MPLBACKEND": "tkagg"}), (["wx"], {"MPLBACKEND": "wx"}), (["wx"], {"MPLBACKEND": "wxagg"}), @@ -45,6 +45,12 @@ def _get_testable_interactive_backends(): reason = "{} cannot be imported".format(", ".join(missing)) elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" + elif env["MPLBACKEND"].startswith('gtk'): + import gi + version = env["MPLBACKEND"][3] + repo = gi.Repository.get_default() + if f'{version}.0' not in repo.enumerate_versions('Gtk'): + reason = "no usable GTK bindings" marks = [] if reason: marks.append(pytest.mark.skip( @@ -87,7 +93,7 @@ def _test_interactive_impl(): assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises - if backend.endswith("agg") and not backend.startswith(("gtk3", "web")): + if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. plt.figure() diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 055c4acb7642..00a6f52698e2 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -613,7 +613,7 @@ def test_mappable_2d_alpha(): # the original alpha array assert cb.alpha is None assert pc.get_alpha() is x - fig.draw_no_output() + fig.draw_without_rendering() def test_colorbar_label(): @@ -766,7 +766,7 @@ def test_inset_colorbar_layout(): cax = ax.inset_axes([1.02, 0.1, 0.03, 0.8]) cb = fig.colorbar(pc, cax=cax) - fig.draw_no_output() + fig.draw_without_rendering() # make sure this is in the figure. In the colorbar swapping # it was being dropped from the list of children... np.testing.assert_allclose(cb.ax.get_position().bounds, @@ -806,7 +806,7 @@ def test_aspects(): pc = ax[mm, nn].pcolormesh(np.arange(100).reshape(10, 10)) cb[nn][mm] = fig.colorbar(pc, ax=ax[mm, nn], orientation=orient, aspect=aspect, extend=extend) - fig.draw_no_output() + fig.draw_without_rendering() # check the extends are right ratio: np.testing.assert_almost_equal(cb[0][1].ax.get_position().height, cb[0][0].ax.get_position().height * 0.9, @@ -827,3 +827,30 @@ def test_aspects(): np.testing.assert_almost_equal( cb[1][0].ax.get_position(original=False).height * 2, cb[1][2].ax.get_position(original=False).height, decimal=2) + + +@image_comparison(['proportional_colorbars.png'], remove_text=True, + style='mpl20') +def test_proportional_colorbars(): + + x = y = np.arange(-3.0, 3.01, 0.025) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + levels = [-1.25, -0.5, -0.125, 0.125, 0.5, 1.25] + cmap = mcolors.ListedColormap( + ['0.3', '0.5', 'white', 'lightblue', 'steelblue']) + cmap.set_under('darkred') + cmap.set_over('crimson') + norm = mcolors.BoundaryNorm(levels, cmap.N) + + extends = ['neither', 'both'] + spacings = ['uniform', 'proportional'] + fig, axs = plt.subplots(2, 2) + for i in range(2): + for j in range(2): + CS3 = axs[i, j].contourf(X, Y, Z, levels, cmap=cmap, norm=norm, + extend=extends[i]) + fig.colorbar(CS3, spacing=spacings[j], ax=axs[i, j]) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index ae004e957591..bf89a3a82364 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1,5 +1,6 @@ import copy import itertools +import unittest.mock from io import BytesIO import numpy as np @@ -17,7 +18,7 @@ import matplotlib.cbook as cbook import matplotlib.pyplot as plt import matplotlib.scale as mscale -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal @pytest.mark.parametrize('N, result', [ @@ -1408,3 +1409,69 @@ def test_norm_deepcopy(): norm2 = copy.deepcopy(norm) assert norm2._scale is None assert norm2.vmin == norm.vmin + + +def test_norm_callback(): + increment = unittest.mock.Mock(return_value=None) + + norm = mcolors.Normalize() + norm.callbacks.connect('changed', increment) + # Haven't updated anything, so call count should be 0 + assert increment.call_count == 0 + + # Now change vmin and vmax to test callbacks + norm.vmin = 1 + assert increment.call_count == 1 + norm.vmax = 5 + assert increment.call_count == 2 + # callback shouldn't be called if setting to the same value + norm.vmin = 1 + assert increment.call_count == 2 + norm.vmax = 5 + assert increment.call_count == 2 + + +def test_scalarmappable_norm_update(): + norm = mcolors.Normalize() + sm = matplotlib.cm.ScalarMappable(norm=norm, cmap='plasma') + # sm doesn't have a stale attribute at first, set it to False + sm.stale = False + # The mappable should be stale after updating vmin/vmax + norm.vmin = 5 + assert sm.stale + sm.stale = False + norm.vmax = 5 + assert sm.stale + sm.stale = False + norm.clip = True + assert sm.stale + # change to the CenteredNorm and TwoSlopeNorm to test those + # Also make sure that updating the norm directly and with + # set_norm both update the Norm callback + norm = mcolors.CenteredNorm() + sm.norm = norm + sm.stale = False + norm.vcenter = 1 + assert sm.stale + norm = mcolors.TwoSlopeNorm(vcenter=0, vmin=-1, vmax=1) + sm.set_norm(norm) + sm.stale = False + norm.vcenter = 1 + assert sm.stale + + +@check_figures_equal() +def test_norm_update_figs(fig_test, fig_ref): + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + z = np.arange(100).reshape((10, 10)) + ax_ref.imshow(z, norm=mcolors.Normalize(10, 90)) + + # Create the norm beforehand with different limits and then update + # after adding to the plot + norm = mcolors.Normalize(0, 1) + ax_test.imshow(z, norm=norm) + # Force initial draw to make sure it isn't already stale + fig_test.canvas.draw() + norm.vmin, norm.vmax = 10, 90 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 007fac6ec1f9..a8222a73d5ee 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -128,7 +128,7 @@ def test_constrained_layout7(): for gs in gsl: fig.add_subplot(gs) # need to trigger a draw to get warning - fig.draw_no_output() + fig.draw_without_rendering() @image_comparison(['constrained_layout8.png']) @@ -309,7 +309,7 @@ def test_constrained_layout18(): ax2 = ax.twinx() example_plot(ax) example_plot(ax2, fontsize=24) - fig.draw_no_output() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -321,7 +321,7 @@ def test_constrained_layout19(): example_plot(ax2, fontsize=24) ax2.set_title('') ax.set_title('') - fig.draw_no_output() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -341,11 +341,11 @@ def test_constrained_layout21(): fig, ax = plt.subplots(constrained_layout=True) fig.suptitle("Suptitle0") - fig.draw_no_output() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle1") - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -355,11 +355,11 @@ def test_constrained_layout22(): """#11035: suptitle should not be include in CL if manually positioned""" fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle", y=0.5) - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -407,7 +407,7 @@ def test_hidden_axes(): # (as does a gridspec slot that is empty) fig, axs = plt.subplots(2, 2, constrained_layout=True) axs[0, 1].set_visible(False) - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(axs[0, 0].get_position().extents) np.testing.assert_allclose( @@ -433,7 +433,7 @@ def test_colorbar_align(): fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.1, wspace=0.1) - fig.draw_no_output() + fig.draw_without_rendering() if location in ['left', 'right']: np.testing.assert_allclose(cbs[0].ax.get_position().x0, cbs[2].ax.get_position().x0) @@ -475,7 +475,7 @@ def test_colorbars_no_overlapH(): def test_manually_set_position(): fig, axs = plt.subplots(1, 2, constrained_layout=True) axs[0].set_position([0.2, 0.2, 0.3, 0.3]) - fig.draw_no_output() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]]) @@ -483,7 +483,7 @@ def test_manually_set_position(): axs[0].set_position([0.2, 0.2, 0.3, 0.3]) pc = axs[0].pcolormesh(np.random.rand(20, 20)) fig.colorbar(pc, ax=axs[0]) - fig.draw_no_output() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]]) @@ -528,7 +528,7 @@ def test_align_labels(): fig.align_ylabels(axs=(ax3, ax1, ax2)) - fig.draw_no_output() + fig.draw_without_rendering() after_align = [ax1.yaxis.label.get_window_extent(), ax2.yaxis.label.get_window_extent(), ax3.yaxis.label.get_window_extent()] @@ -541,22 +541,22 @@ def test_align_labels(): def test_suplabels(): fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) fig.supxlabel('Boo') fig.supylabel('Booy') - fig.draw_no_output() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) # check that specifying x (y) doesn't ruin the layout fig.supxlabel('Boo', x=0.5) fig.supylabel('Boo', y=0.5) - fig.draw_no_output() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index c440003f49c9..3a38516c7272 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -76,7 +76,7 @@ def test_date_empty(): # http://sourceforge.net/tracker/?func=detail&aid=2850075&group_id=80706&atid=560720 fig, ax = plt.subplots() ax.xaxis_date() - fig.draw_no_output() + fig.draw_without_rendering() np.testing.assert_allclose(ax.get_xlim(), [mdates.date2num(np.datetime64('2000-01-01')), mdates.date2num(np.datetime64('2010-01-01'))]) @@ -85,7 +85,7 @@ def test_date_empty(): mdates.set_epoch('0000-12-31') fig, ax = plt.subplots() ax.xaxis_date() - fig.draw_no_output() + fig.draw_without_rendering() np.testing.assert_allclose(ax.get_xlim(), [mdates.date2num(np.datetime64('2000-01-01')), mdates.date2num(np.datetime64('2010-01-01'))]) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 7a222f20a058..317528879304 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -412,11 +412,11 @@ def test_autofmt_xdate(which): @mpl.style.context('default') def test_change_dpi(): fig = plt.figure(figsize=(4, 4)) - fig.draw_no_output() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 400 assert fig.canvas.renderer.width == 400 fig.dpi = 50 - fig.draw_no_output() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 200 assert fig.canvas.renderer.width == 200 @@ -1082,10 +1082,10 @@ def test_subfigure_ticks(): ax3 = subfig_bl.add_subplot(gs[0, 3:14], sharey=ax1) fig.set_dpi(120) - fig.draw_no_output() + fig.draw_without_rendering() ticks120 = ax2.get_xticks() fig.set_dpi(300) - fig.draw_no_output() + fig.draw_without_rendering() ticks300 = ax2.get_xticks() np.testing.assert_allclose(ticks120, ticks300) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 4cad797b3757..ee62120c1e37 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -101,7 +101,7 @@ def test_utf16m_sfnt(): entry = next(entry for entry in fontManager.ttflist if Path(entry.fname).name == "seguisbi.ttf") except StopIteration: - pytest.skip("Couldn't find font to test against.") + pytest.skip("Couldn't find seguisbi.ttf font to test against.") else: # Check that we successfully read "semibold" from the font's sfnt table # and set its weight accordingly. @@ -111,14 +111,25 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) if Path(findfont(fp)).name != "wqy-zenhei.ttc": - pytest.skip("Font may be missing") - + pytest.skip("Font wqy-zenhei.ttc may be missing") fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: fig.savefig(BytesIO(), format=fmt) +def test_find_noto(): + fp = FontProperties(family=["Noto Sans CJK SC", "Noto Sans CJK JP"]) + name = Path(findfont(fp)).name + if name not in ("NotoSansCJKsc-Regular.otf", "NotoSansCJK-Regular.ttc"): + pytest.skip(f"Noto Sans CJK SC font may be missing (found {name})") + + fig, ax = plt.subplots() + ax.text(0.5, 0.5, 'Hello, 你好', fontproperties=fp) + for fmt in ["raw", "svg", "pdf", "ps"]: + fig.savefig(BytesIO(), format=fmt) + + def test_find_invalid(tmpdir): tmp_path = Path(tmpdir) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 37dddd4e4706..2e7fae6c58d8 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1017,8 +1017,8 @@ def test_imshow_bool(): def test_full_invalid(): fig, ax = plt.subplots() ax.imshow(np.full((10, 10), np.nan)) - with pytest.warns(UserWarning): - fig.canvas.draw() + + fig.canvas.draw() @pytest.mark.parametrize("fmt,counted", diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 768db940c756..0055d54a03a8 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -402,7 +402,7 @@ def test_default_math_fontfamily(): prop2 = text2.get_fontproperties() assert prop2.get_math_fontfamily() == 'cm' - fig.draw_no_output() + fig.draw_without_rendering() def test_argument_order(): @@ -427,7 +427,7 @@ def test_argument_order(): prop4 = text4.get_fontproperties() assert prop4.get_math_fontfamily() == 'dejavusans' - fig.draw_no_output() + fig.draw_without_rendering() def test_mathtext_cmr10_minus_sign(): diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 17f63f737d84..610ff9b80b40 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -508,3 +508,39 @@ def test_backend_fallback_headful(tmpdir): # The actual backend will depend on what's installed, but at least tkagg is # present. assert backend.strip().lower() != "agg" + + +def test_deprecation(monkeypatch): + monkeypatch.setitem( + mpl._deprecated_map, "patch.linewidth", + ("0.0", "axes.linewidth", lambda old: 2 * old, lambda new: new / 2)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.linewidth"] \ + == mpl.rcParams["axes.linewidth"] / 2 + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.linewidth"] = 1 + assert mpl.rcParams["axes.linewidth"] == 2 + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.edgecolor", + ("0.0", "axes.edgecolor")) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.edgecolor"] \ + == mpl.rcParams["axes.edgecolor"] + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.edgecolor"] = "#abcd" + assert mpl.rcParams["axes.edgecolor"] != "#abcd" + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.force_edgecolor", + ("0.0", None)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.force_edgecolor"] is None + + monkeypatch.setitem( + mpl._deprecated_remain_as_none, "svg.hashsalt", + ("0.0",)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["svg.hashsalt"] = "foobar" + assert mpl.rcParams["svg.hashsalt"] == "foobar" # Doesn't warn. + mpl.rcParams["svg.hashsalt"] = None # Doesn't warn. diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index c7a06e0e32ac..a6f6b44c9707 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -226,17 +226,17 @@ def test_empty_default_limits(quantity_converter): munits.registry[Quantity] = quantity_converter fig, ax1 = plt.subplots() ax1.xaxis.update_units(Quantity([10], "miles")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax1.get_xlim() == (0, 100) ax1.yaxis.update_units(Quantity([10], "miles")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax1.get_ylim() == (0, 100) fig, ax = plt.subplots() ax.axhline(30) ax.plot(Quantity(np.arange(0, 3), "miles"), Quantity(np.arange(0, 6, 2), "feet")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 2) assert ax.get_ylim() == (0, 30) @@ -244,20 +244,20 @@ def test_empty_default_limits(quantity_converter): ax.axvline(30) ax.plot(Quantity(np.arange(0, 3), "miles"), Quantity(np.arange(0, 6, 2), "feet")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 30) assert ax.get_ylim() == (0, 4) fig, ax = plt.subplots() ax.xaxis.update_units(Quantity([10], "miles")) ax.axhline(30) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 100) assert ax.get_ylim() == (28.5, 31.5) fig, ax = plt.subplots() ax.yaxis.update_units(Quantity([10], "miles")) ax.axvline(30) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_ylim() == (0, 100) assert ax.get_xlim() == (28.5, 31.5) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index a43cfec6191f..f50402a20a15 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -619,7 +619,11 @@ def test_CheckButtons(): check.disconnect(cid) -def test_TextBox(): +@pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) +def test_TextBox(toolbar): + # Avoid "toolmanager is provisional" warning. + dict.__setitem__(plt.rcParams, "toolbar", toolbar) + from unittest.mock import Mock submit_event = Mock() text_change_event = Mock() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index fc30977d9b39..7a0380453452 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -118,9 +118,9 @@ the input ``str``. For function input, a `.FuncFormatter` with the input function will be generated and used. -See :doc:`/gallery/ticks_and_spines/major_minor_demo` for an -example of setting major and minor ticks. See the :mod:`matplotlib.dates` -module for more information and examples of using date locators and formatters. +See :doc:`/gallery/ticks/major_minor_demo` for an example of setting major +and minor ticks. See the :mod:`matplotlib.dates` module for more information +and examples of using date locators and formatters. """ import itertools diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 5904ebc1fa1c..bd58833baf88 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -57,11 +57,9 @@ def restore_bbox(): tr = Affine2D().scale(fixed_dpi) dpi_scale = fixed_dpi / fig.dpi - _bbox = TransformedBbox(bbox_inches, tr) - fig.bbox_inches = Bbox.from_bounds(0, 0, bbox_inches.width, bbox_inches.height) - x0, y0 = _bbox.x0, _bbox.y0 + x0, y0 = tr.transform(bbox_inches.p0) w1, h1 = fig.bbox.width * dpi_scale, fig.bbox.height * dpi_scale fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) fig.transFigure.invalidate() diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 809b970915a9..66283c7d7348 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -13,7 +13,7 @@ from matplotlib import _api, rcParams from matplotlib.font_manager import FontProperties -from matplotlib.transforms import TransformedBbox, Bbox +from matplotlib.transforms import Bbox def _auto_adjust_subplotpars( @@ -84,8 +84,7 @@ def _auto_adjust_subplotpars( bb += [ax.get_tightbbox(renderer)] tight_bbox_raw = Bbox.union(bb) - tight_bbox = TransformedBbox(tight_bbox_raw, - fig.transFigure.inverted()) + tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw) hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 155b3b3f7b15..a59b3b1b1677 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1287,7 +1287,7 @@ def begin_typing(self, x): # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. toolmanager.keypresslock(self) - stack.push(toolmanager.keypresslock.release, self) + stack.callback(toolmanager.keypresslock.release, self) else: # If not using toolmanager, disable all keypress-related rcParams. # Avoid spurious warnings if keymaps are getting deprecated. @@ -2710,7 +2710,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) - "move": Move the existing shape, default: no modifier. - "clear": Clear the current shape, default: "escape". - - "square": Makes the shape square, default: "shift". + - "square": Make the shape square, default: "shift". - "center": Make the initial point the center of the shape, default: "ctrl". diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 5193cc540c31..fcdb32851b8c 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -126,7 +126,7 @@ def __init__(self, parent_axes, zoom, loc, bbox_transform=bbox_transform) def get_extent(self, renderer): - bb = TransformedBbox(self.axes.viewLim, self.parent_axes.transData) + bb = self.parent_axes.transData.transform_bbox(self.axes.viewLim) fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) pad = self.pad * fontsize return (abs(bb.width * self.zoom) + 2 * pad, diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index a140be9ef260..b2ef0b1c94d3 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -187,12 +187,6 @@ def get_aux_axes(self, tr=None, viewlim_mode="equal", axes_class=Axes): ax2._remove_method = self.parasites.remove return ax2 - def _get_legend_handles(self, legend_handler_map=None): - all_handles = super()._get_legend_handles() - for ax in self.parasites: - all_handles.extend(ax._get_legend_handles(legend_handler_map)) - return all_handles - def draw(self, renderer): orig_children_len = len(self._children) diff --git a/lib/mpl_toolkits/axisartist/axisline_style.py b/lib/mpl_toolkits/axisartist/axisline_style.py index 80f3ce58eb48..db4b0c144c5e 100644 --- a/lib/mpl_toolkits/axisartist/axisline_style.py +++ b/lib/mpl_toolkits/axisartist/axisline_style.py @@ -9,9 +9,7 @@ class _FancyAxislineStyle: class SimpleArrow(FancyArrowPatch): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for SimpleArrow style.""" _ARROW_STYLE = "->" def __init__(self, axis_artist, line_path, transform, @@ -69,9 +67,7 @@ def draw(self, renderer): FancyArrowPatch.draw(self, renderer) class FilledArrow(SimpleArrow): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for SimpleArrow style.""" _ARROW_STYLE = "-|>" diff --git a/mplsetup.cfg.template b/mplsetup.cfg.template index 2fd28a6e4d67..6c54a23fdccb 100644 --- a/mplsetup.cfg.template +++ b/mplsetup.cfg.template @@ -28,8 +28,8 @@ [rc_options] # User-configurable options # -# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, MacOSX, Pdf, Ps, -# QtAgg, QtCairo, SVG, TkAgg, WX, WXAgg. +# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, +# MacOSX, Pdf, Ps, QtAgg, QtCairo, SVG, TkAgg, WX, WXAgg. # # The Agg, Ps, Pdf and SVG backends do not require external dependencies. Do # not choose MacOSX if you have disabled the relevant extension modules. The diff --git a/plot_types/basic/pie.py b/plot_types/basic/pie.py index e20b6c5001db..b078a8f5e6ab 100644 --- a/plot_types/basic/pie.py +++ b/plot_types/basic/pie.py @@ -1,6 +1,6 @@ """ ====== -pie(X) +pie(x) ====== See `~matplotlib.axes.Axes.pie`. diff --git a/plot_types/basic/plot.py b/plot_types/basic/plot.py index cd608f2c33f9..ac93ecf71b56 100644 --- a/plot_types/basic/plot.py +++ b/plot_types/basic/plot.py @@ -1,6 +1,6 @@ """ ========== -plot(X, Y) +plot(x, y) ========== See `~matplotlib.axes.Axes.plot`. diff --git a/plot_types/basic/scatter_plot.py b/plot_types/basic/scatter_plot.py index bc9ec24bbb78..68f06744150c 100644 --- a/plot_types/basic/scatter_plot.py +++ b/plot_types/basic/scatter_plot.py @@ -1,6 +1,6 @@ """ ============= -scatter(X, Y) +scatter(x, y) ============= See `~matplotlib.axes.Axes.scatter`. diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 2cfba0dbad07..8be10d4d107c 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -12,12 +12,13 @@ colorspacious ipython ipywidgets numpydoc>=0.8 -pydata-sphinx-theme>=0.5.0 +pydata-sphinx-theme>=0.6.0 sphinxcontrib-svg2pdfconverter>=1.1.0 -# sphinx-gallery>=0.7 -# b41e328 is PR 808 which adds the image_srcset directive. When this is +# sphinx-gallery>=0.7 +# b41e328 is PR 808 which adds the image_srcset directive. When this is # released with sphinx gallery, we can change to the last release w/o this feature: # sphinx-gallery>0.90 git+git://github.com/sphinx-gallery/sphinx-gallery@b41e328#egg=sphinx-gallery sphinx-copybutton +sphinx-panels scipy diff --git a/tutorials/introductory/sample_plots.py b/tutorials/introductory/sample_plots.py index 91ae19eb0015..003bc70661ff 100644 --- a/tutorials/introductory/sample_plots.py +++ b/tutorials/introductory/sample_plots.py @@ -338,6 +338,7 @@ For examples of how to embed Matplotlib in different toolkits, see: + * :doc:`/gallery/user_interfaces/embedding_in_gtk4_sgskip` * :doc:`/gallery/user_interfaces/embedding_in_gtk3_sgskip` * :doc:`/gallery/user_interfaces/embedding_in_wx2_sgskip` * :doc:`/gallery/user_interfaces/mpl_with_glade3_sgskip` diff --git a/tutorials/introductory/usage.py b/tutorials/introductory/usage.py index 08b4d6ad00a0..17e623399b65 100644 --- a/tutorials/introductory/usage.py +++ b/tutorials/introductory/usage.py @@ -300,9 +300,10 @@ def my_plotter(ax, data1, data2, param_dict): # Without a backend explicitly set, Matplotlib automatically detects a usable # backend based on what is available on your system and on whether a GUI event # loop is already running. The first usable backend in the following list is -# selected: MacOSX, Qt5Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, is a -# non-interactive backend that can only write to files. It is used on Linux, -# if Matplotlib cannot connect to either an X display or a Wayland display. +# selected: MacOSX, QtAgg, GTK4Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, +# is a non-interactive backend that can only write to files. It is used on +# Linux, if Matplotlib cannot connect to either an X display or a Wayland +# display. # # Here is a detailed description of the configuration methods: # @@ -370,7 +371,7 @@ def my_plotter(ax, data1, data2, param_dict): # from the canvas (the place where the drawing goes). The canonical # renderer for user interfaces is ``Agg`` which uses the `Anti-Grain # Geometry`_ C++ library to make a raster (pixel) image of the figure; it -# is used by the ``QtAgg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and +# is used by the ``QtAgg``, ``GTK4Agg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and # ``macosx`` backends. An alternative renderer is based on the Cairo library, # used by ``QtCairo``, etc. # @@ -419,6 +420,9 @@ def my_plotter(ax, data1, data2, param_dict): # GTK3Agg Agg rendering to a GTK_ 3.x canvas (requires PyGObject_, # and pycairo_ or cairocffi_). This backend can be activated in # IPython with ``%matplotlib gtk3``. +# GTK4Agg Agg rendering to a GTK_ 4.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). This backend can be activated in +# IPython with ``%matplotlib gtk4``. # macosx Agg rendering into a Cocoa canvas in OSX. This backend can be # activated in IPython with ``%matplotlib osx``. # TkAgg Agg rendering to a Tk_ canvas (requires TkInter_). This @@ -430,6 +434,8 @@ def my_plotter(ax, data1, data2, param_dict): # figure. # GTK3Cairo Cairo rendering to a GTK_ 3.x canvas (requires PyGObject_, # and pycairo_ or cairocffi_). +# GTK4Cairo Cairo rendering to a GTK_ 4.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). # wxAgg Agg rendering to a wxWidgets_ canvas (requires wxPython_ 4). # This backend can be activated in IPython with ``%matplotlib wx``. # ========= ================================================================