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 @@
+
+
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 @@
-
-
-
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``.
# ========= ================================================================