From 2ef064cb32d873d9000596661b0e3945e73a044f Mon Sep 17 00:00:00 2001 From: SidharthBansal Date: Fri, 17 Apr 2020 03:14:33 +0530 Subject: [PATCH] Removed pyplot.plotting --- .flake8 | 20 +- .gitignore | 2 + .travis.yml | 2 +- INSTALL.rst | 4 +- README.rst | 2 +- doc/api/animation_api.rst | 6 +- doc/api/api_changes_3.3/behaviour.rst | 27 +- doc/api/api_changes_3.3/deprecations.rst | 36 +- doc/api/axes_api.rst | 7 +- doc/api/toolkits/mplot3d/faq.rst | 8 +- doc/conf.py | 10 +- doc/devel/MEP/MEP11.rst | 2 +- doc/devel/MEP/MEP12.rst | 6 +- doc/devel/coding_guide.rst | 15 + doc/devel/release_guide.rst | 2 +- doc/index.rst | 4 +- doc/missing-references.json | 20 - doc/sphinxext/github.py | 18 +- doc/thirdpartypackages/index.rst | 12 +- doc/users/history.rst | 2 +- .../next_whats_new/2020-01-24-lateshare.rst | 8 + .../2020-03-15-cursor-sigdigits.rst | 6 + .../2020-04-11-constrainedlayout-not-exp.rst | 7 + doc/users/next_whats_new/2020-04-13-AL.rst | 7 + .../2020-04-14-title-yparam.rst | 8 + doc/users/prev_whats_new/whats_new_1.0.rst | 2 +- doc/users/prev_whats_new/whats_new_1.3.rst | 2 +- doc/users/prev_whats_new/whats_new_1.4.rst | 2 +- doc/users/prev_whats_new/whats_new_1.5.rst | 2 +- doc/users/prev_whats_new/whats_new_2.2.rst | 2 +- examples/axisartist/demo_floating_axis.py | 3 +- .../contour_demo.py | 12 +- .../images_contours_and_fields/multi_image.py | 5 +- .../pcolormesh_grids.py | 2 +- .../fill_between_demo.py | 4 +- .../lines_bars_and_markers/filled_step.py | 2 +- examples/misc/demo_agg_filter.py | 2 +- examples/misc/image_thumbnail_sgskip.py | 2 +- examples/mplot3d/contour3d.py | 2 +- examples/mplot3d/contour3d_2.py | 2 +- examples/mplot3d/contourf3d.py | 2 +- examples/showcase/anatomy.py | 2 +- examples/showcase/mandelbrot.py | 2 +- examples/specialty_plots/radar_chart.py | 3 +- examples/statistics/boxplot_vs_violin.py | 2 +- examples/statistics/confidence_ellipse.py | 7 +- examples/statistics/customized_violin.py | 2 +- examples/statistics/violinplot.py | 2 +- .../style_sheets/style_sheets_reference.py | 21 +- .../subplots_demo.py | 19 +- .../text_labels_and_annotations/arrow_demo.py | 3 +- .../custom_legends.py | 4 +- .../demo_annotation_box.py | 3 +- .../titles_demo.py | 49 ++- .../user_interfaces/embedding_in_tk_sgskip.py | 9 +- examples/userdemo/demo_gridspec06.py | 46 +- .../widgets/lasso_selector_demo_sgskip.py | 3 +- examples/widgets/textbox.py | 18 +- lib/matplotlib/__init__.py | 17 +- lib/matplotlib/_constrained_layout.py | 22 +- lib/matplotlib/afm.py | 16 +- lib/matplotlib/animation.py | 27 +- lib/matplotlib/artist.py | 13 +- lib/matplotlib/axes/_axes.py | 27 +- lib/matplotlib/axes/_base.py | 269 ++++++------ lib/matplotlib/axes/_subplots.py | 7 +- lib/matplotlib/axis.py | 7 +- lib/matplotlib/backend_bases.py | 287 ++++++------ lib/matplotlib/backend_tools.py | 14 +- lib/matplotlib/backends/_backend_tk.py | 24 +- lib/matplotlib/backends/backend_gtk3.py | 12 +- lib/matplotlib/backends/backend_gtk3agg.py | 3 +- lib/matplotlib/backends/backend_pdf.py | 19 +- lib/matplotlib/backends/backend_pgf.py | 6 +- lib/matplotlib/backends/backend_ps.py | 25 +- lib/matplotlib/backends/backend_qt5.py | 79 ++-- lib/matplotlib/backends/backend_svg.py | 14 +- lib/matplotlib/backends/backend_template.py | 2 +- lib/matplotlib/backends/backend_wx.py | 65 ++- lib/matplotlib/backends/qt_compat.py | 11 +- .../backends/qt_editor/_formlayout.py | 2 +- .../backends/qt_editor/figureoptions.py | 5 +- .../backends/web_backend/.eslintrc.js | 21 + .../backends/web_backend/.prettierignore | 7 + .../backends/web_backend/.prettierrc | 11 + lib/matplotlib/backends/web_backend/js/mpl.js | 410 ++++++++++-------- .../backends/web_backend/js/mpl_tornado.js | 1 + .../backends/web_backend/js/nbagg_mpl.js | 155 ++++--- .../backends/web_backend/package.json | 15 + lib/matplotlib/blocking_input.py | 5 +- lib/matplotlib/cbook/__init__.py | 61 ++- lib/matplotlib/cm.py | 10 +- lib/matplotlib/collections.py | 16 +- lib/matplotlib/colorbar.py | 2 - lib/matplotlib/colors.py | 16 +- lib/matplotlib/contour.py | 74 ++-- lib/matplotlib/dates.py | 14 +- lib/matplotlib/dviread.py | 27 +- lib/matplotlib/figure.py | 2 +- lib/matplotlib/font_manager.py | 7 +- lib/matplotlib/gridspec.py | 7 +- lib/matplotlib/image.py | 2 +- lib/matplotlib/legend.py | 6 +- lib/matplotlib/legend_handler.py | 7 +- lib/matplotlib/lines.py | 10 +- lib/matplotlib/markers.py | 11 +- lib/matplotlib/mathtext.py | 6 +- lib/matplotlib/mlab.py | 17 +- lib/matplotlib/mpl-data/images/help.gif | Bin 0 -> 564 bytes .../mpl-data/stylelib/classic.mplstyle | 3 +- lib/matplotlib/patches.py | 58 +-- lib/matplotlib/path.py | 13 +- lib/matplotlib/projections/geo.py | 2 +- lib/matplotlib/projections/polar.py | 14 +- lib/matplotlib/pyplot.py | 21 +- lib/matplotlib/quiver.py | 172 ++++---- lib/matplotlib/rcsetup.py | 5 +- lib/matplotlib/streamplot.py | 18 +- lib/matplotlib/style/core.py | 11 +- lib/matplotlib/table.py | 88 ++-- lib/matplotlib/testing/disable_internet.py | 8 +- lib/matplotlib/testing/jpl_units/Duration.py | 25 +- lib/matplotlib/testing/jpl_units/Epoch.py | 15 +- .../testing/jpl_units/EpochConverter.py | 47 +- .../testing/jpl_units/StrConverter.py | 41 +- lib/matplotlib/testing/jpl_units/UnitDbl.py | 34 +- .../testing/jpl_units/UnitDblConverter.py | 31 +- lib/matplotlib/testing/jpl_units/__init__.py | 4 +- .../bbox_inches_tight_raster.pdf | Bin 7455 -> 7245 bytes .../test_usetex/test_usetex.pdf | Bin 174976 -> 125933 bytes .../test_usetex/test_usetex.png | Bin 9329 -> 13512 bytes lib/matplotlib/tests/test_axes.py | 5 +- lib/matplotlib/tests/test_backend_bases.py | 47 +- .../tests/test_backends_interactive.py | 12 +- lib/matplotlib/tests/test_category.py | 3 +- lib/matplotlib/tests/test_compare_images.py | 3 +- lib/matplotlib/tests/test_gridspec.py | 6 + lib/matplotlib/tests/test_legend.py | 7 + lib/matplotlib/tests/test_text.py | 5 +- lib/matplotlib/tests/test_ticker.py | 10 + lib/matplotlib/tests/test_tightlayout.py | 4 +- lib/matplotlib/tests/test_usetex.py | 34 +- lib/matplotlib/tests/test_widgets.py | 3 +- lib/matplotlib/tests/tinypages/range6.py | 4 +- lib/matplotlib/texmanager.py | 30 +- lib/matplotlib/text.py | 3 +- lib/matplotlib/ticker.py | 63 ++- lib/matplotlib/tight_bbox.py | 6 +- lib/matplotlib/tight_layout.py | 7 +- lib/matplotlib/transforms.py | 102 ++++- lib/matplotlib/tri/triinterpolate.py | 131 +++--- lib/matplotlib/tri/trirefine.py | 10 +- lib/matplotlib/tri/tritools.py | 13 +- lib/matplotlib/type1font.py | 5 +- lib/matplotlib/units.py | 8 +- lib/matplotlib/widgets.py | 293 ++++++------- .../axes_grid1/anchored_artists.py | 60 +-- lib/mpl_toolkits/axes_grid1/axes_divider.py | 39 +- lib/mpl_toolkits/axes_grid1/axes_rgb.py | 9 +- lib/mpl_toolkits/axes_grid1/axes_size.py | 2 +- lib/mpl_toolkits/axes_grid1/inset_locator.py | 8 +- lib/mpl_toolkits/mplot3d/art3d.py | 14 +- lib/mpl_toolkits/mplot3d/axes3d.py | 35 +- matplotlibrc.template | 5 +- setup.py | 16 +- tutorials/intermediate/color_cycle.py | 2 +- .../intermediate/constrainedlayout_guide.py | 11 +- tutorials/intermediate/gridspec.py | 43 +- tutorials/intermediate/tight_layout_guide.py | 6 +- tutorials/introductory/customizing.py | 4 +- tutorials/introductory/lifecycle.py | 2 +- tutorials/text/text_intro.py | 3 +- 172 files changed, 2190 insertions(+), 1870 deletions(-) create mode 100644 doc/users/next_whats_new/2020-01-24-lateshare.rst create mode 100644 doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst create mode 100644 doc/users/next_whats_new/2020-04-11-constrainedlayout-not-exp.rst create mode 100644 doc/users/next_whats_new/2020-04-13-AL.rst create mode 100644 doc/users/next_whats_new/2020-04-14-title-yparam.rst create mode 100644 lib/matplotlib/backends/web_backend/.eslintrc.js create mode 100644 lib/matplotlib/backends/web_backend/.prettierignore create mode 100644 lib/matplotlib/backends/web_backend/.prettierrc create mode 100644 lib/matplotlib/backends/web_backend/package.json create mode 100644 lib/matplotlib/mpl-data/images/help.gif diff --git a/.flake8 b/.flake8 index 6db0a4fb3c5f..cd38366d0969 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,19 @@ [flake8] max-line-length = 79 +select = + # flake8 default + C90, E, F, W, + # docstring-convention=numpy + D100, D101, D102, D103, D104, D105, D106, + D200, D201, D202, D204, D205, D206, D207, D208, + D209, D210, D211, D214, D215, + D300, D301, D302, + D400, D401, D403, D404, D405, D406, D407, D408, + D409, D410, D411, D412, D414, + # matplotlib-specific extra pydocstyle errors + D213, ignore = - # Normal default + # flake8 default E121,E123,E126,E226,E24,E704,W503,W504, # Additional ignores: E122, E127, E131, @@ -13,9 +25,9 @@ ignore = N801, N802, N803, N806, N812, # pydocstyle D100, D101, D102, D103, D104, D105, D106, D107, - D200, D202, D203, D204, D205, D207, D212, D213, - D301 - D400, D401, D402, D403, D413, + D200, D202, D203, D204, D205, D207, D212, + D301, + D400, D401, D402, D403, D404, D413, exclude = .git diff --git a/.gitignore b/.gitignore index bbd62a042419..491c72e229b5 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,5 @@ lib/z.lib ######################### jquery-ui-*/ +lib/matplotlib/backends/web_backend/node_modules/ +lib/matplotlib/backends/web_backend/package-lock.json diff --git a/.travis.yml b/.travis.yml index 09482b556202..3d4acde37434 100644 --- a/.travis.yml +++ b/.travis.yml @@ -192,7 +192,7 @@ script: fi - | if [[ $RUN_FLAKE8 == 1 ]]; then - flake8 --statistics && echo "Flake8 passed without any issues!" + flake8 --docstring-convention=all --statistics && echo "Flake8 passed without any issues!" fi before_cache: | diff --git a/INSTALL.rst b/INSTALL.rst index 3d5d742dd6b1..6e6140cf44c4 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -108,9 +108,9 @@ Dependencies Matplotlib requires the following dependencies: * `Python `_ (>= 3.6) -* `NumPy `_ (>= 1.15) +* `NumPy `_ (>= 1.15) * `setuptools `_ -* `cycler `_ (>= 0.10.0) +* `cycler `_ (>= 0.10.0) * `dateutil `_ (>= 2.1) * `kiwisolver `_ (>= 1.0.0) * `Pillow `_ (>= 6.2) diff --git a/README.rst b/README.rst index 6d39140b046e..242ee716b703 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ .. _Downloads: https://pepy.tech/project/matplotlib/month .. |NUMFocus| image:: https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A -.. _NUMFocus: https://www.numfocus.org +.. _NUMFocus: https://numfocus.org .. image:: https://matplotlib.org/_static/logo2.svg diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index dd4bad8f0518..89341db8d915 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -212,9 +212,9 @@ at a time and ``finish()`` finalizes the movie and writes the output file to disk. For example :: moviewriter = MovieWriter(...) - moviewriter.setup(fig=fig, 'my_movie.ext', dpi=100) + moviewriter.setup(fig, 'my_movie.ext', dpi=100) for j in range(n): - update_figure(n) + update_figure(j) moviewriter.grab_frame() moviewriter.finish() @@ -223,7 +223,7 @@ strongly encouraged to use the `~MovieWriter.saving` context manager :: with moviewriter.saving(fig, 'myfile.mp4', dpi=100): for j in range(n): - update_figure(n) + update_figure(j) moviewriter.grab_frame() to ensures that setup and cleanup are performed as necessary. diff --git a/doc/api/api_changes_3.3/behaviour.rst b/doc/api/api_changes_3.3/behaviour.rst index 9ebe3fe42240..a85f2974debc 100644 --- a/doc/api/api_changes_3.3/behaviour.rst +++ b/doc/api/api_changes_3.3/behaviour.rst @@ -98,9 +98,9 @@ deprecation warning. `~.Axes.errorbar` now color cycles when only errorbar color is set ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Previously setting the *ecolor* would turn off automatic color cycling for the plot, leading to the -the lines and markers defaulting to whatever the first color in the color cycle was in the case of -multiple plot calls. +Previously setting the *ecolor* would turn off automatic color cycling for the plot, leading to the +the lines and markers defaulting to whatever the first color in the color cycle was in the case of +multiple plot calls. `.rcsetup.validate_color_for_prop_cycle` now always raises TypeError for bytes input ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -155,3 +155,24 @@ support for it will be dropped in a future Matplotlib release. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Previously, keyword arguments were silently ignored when no positional arguments were given. + +`.Axis.get_minorticklabels` and `.Axis.get_majorticklabels` now returns plain list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, `.Axis.get_minorticklabels` and `.Axis.get_majorticklabels` returns +silent_list. Their return type is now changed to normal list. +`.get_xminorticklabels`, `.get_yminorticklabels`, `.get_zminorticklabels`, +`.Axis.get_ticklabels`, `.get_xmajorticklabels`, `.get_ymajorticklabels` and +`.get_zmajorticklabels` methods will be affected by this change. + +Default slider formatter +~~~~~~~~~~~~~~~~~~~~~~~~ +The default method used to format `.Slider` values has been changed to use a +`.ScalarFormatter` adapted the slider values limits. This should ensure that +values are displayed with an appropriate number of significant digits even if +they are much smaller or much bigger than 1. To restore the old behavior, +explicitly pass a "%1.2f" as the *valfmt* parameter to `.Slider`. + +``table.CustomCell`` is now an alias for `.table.Cell` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +All the functionality of ``CustomCell`` has been moved to its base class +`~.table.Cell`. diff --git a/doc/api/api_changes_3.3/deprecations.rst b/doc/api/api_changes_3.3/deprecations.rst index 680f049249e3..b3eda75a6b6a 100644 --- a/doc/api/api_changes_3.3/deprecations.rst +++ b/doc/api/api_changes_3.3/deprecations.rst @@ -366,11 +366,12 @@ The ``Fil``, ``Fill``, ``Filll``, ``NegFil``, ``NegFill``, ``NegFilll``, and ``SsGlue`` classes in the :mod:`matplotlib.mathtext` module are deprecated. As an alternative, directly construct glue instances with ``Glue("fil")``, etc. -NavigationToolbar2QT.parent -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This attribute is deprecated. In order to access the parent window, use +NavigationToolbar2QT.parent and .basedir +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These attributes are deprecated. In order to access the parent window, use ``toolbar.canvas.parent()``. Once the deprecation period is elapsed, it will -also be accessible as ``toolbar.parent()``. +also be accessible as ``toolbar.parent()``. The base directory to the icons +is ``os.path.join(mpl.get_data_path(), "images")``. Path helpers in :mod:`.bezier` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -423,6 +424,33 @@ or None is deprecated; set it to "horizontal" instead. Moreover, the two orientations ("horizontal" and "vertical") will become case-sensitive in the future. +`pyplot.plotting()` deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`pyplot.plotting()` is no-op function. It is now removed. + *minor* kwarg to `.Axis.get_ticklocs` will become keyword-only ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Passing this argument positionally is deprecated. + +Case-insensitive properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Normalization of upper or mixed-case property names to lowercase in +`.Artist.set` and `.Artist.update` is deprecated. In the future, property +names will be passed as is, allowing one to pass names such as *patchA* or +*UVC*. + +``ContourSet.ax``, ``Quiver.ax`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These attributes are deprecated in favor of ``ContourSet.axes`` and +``Quiver.axes``, for consistency with other artists. + +``Locator.refresh()`` and associated methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``Locator.refresh()`` is deprecated. This method was called at certain places +to let locators update their internal state, typically based on the axis +limits. Locators should now always consult the axis limits when called, if +needed. + +The associated helper methods ``NavigationToolbar2.draw()`` and +``ToolViewsPositions.refresh_locators()`` are deprecated, and should be +replaced by calls to ``draw_idle()`` on the corresponding canvas. diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 84817c6d6bba..4e7e4174ffd3 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -447,8 +447,8 @@ Adding Artists Axes.add_table -Twinning -======== +Twinning and sharing +==================== .. autosummary:: :toctree: _as_gen @@ -458,6 +458,9 @@ Twinning Axes.twinx Axes.twiny + Axes.sharex + Axes.sharey + Axes.get_shared_x_axes Axes.get_shared_y_axes diff --git a/doc/api/toolkits/mplot3d/faq.rst b/doc/api/toolkits/mplot3d/faq.rst index cc173edb3581..dfc23b55e069 100644 --- a/doc/api/toolkits/mplot3d/faq.rst +++ b/doc/api/toolkits/mplot3d/faq.rst @@ -4,12 +4,12 @@ mplot3d FAQ *********** -How is mplot3d different from MayaVi? +How is mplot3d different from Mayavi? ===================================== -`MayaVi2 `_ +`Mayavi `_ is a very powerful and featureful 3D graphing library. For advanced 3D scenes and excellent rendering capabilities, it is highly recommended to -use MayaVi2. +use Mayavi. mplot3d was intended to allow users to create simple 3D graphs with the same "look-and-feel" as matplotlib's 2D plots. Furthermore, users can use the same @@ -38,7 +38,7 @@ rendered properly in matplotlib's 2D rendering engine. This problem will likely not be solved until OpenGL support is added to all of the backends (patches are greatly welcomed). Until then, if you need complex 3D scenes, we recommend using -`MayaVi `_. +`MayaVi `_. I don't like how the 3D plot is laid out, how do I change that? diff --git a/doc/conf.py b/doc/conf.py index c14ed27fdfea..76c941b7d7bc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,6 +28,10 @@ # General configuration # --------------------- +# Strip backslahes in function's signature +# To be removed when numpydoc > 0.9.x +strip_signature_backslash = True + # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ @@ -261,8 +265,10 @@ def _check_dependencies(): # Custom sidebar templates, maps page names to templates. html_sidebars = { - 'index': ['sidebar_announcement.html', 'sidebar_versions.html', - 'donate_sidebar.html'], + 'index': [ + # 'sidebar_announcement.html', + 'sidebar_versions.html', + 'donate_sidebar.html'], '**': ['localtoc.html', 'relations.html', 'pagesource.html'] } diff --git a/doc/devel/MEP/MEP11.rst b/doc/devel/MEP/MEP11.rst index acb61e4fa932..3104f75e9d51 100644 --- a/doc/devel/MEP/MEP11.rst +++ b/doc/devel/MEP/MEP11.rst @@ -117,7 +117,7 @@ Implementation For installing from source, and assuming the user has all of the C-level compilers and dependencies, this can be accomplished fairly easily using distribute_ and following the instructions `here -`_. The only anticipated +`_. The only anticipated change to the matplotlib library code will be to import pyparsing_ from the top-level namespace rather than from within matplotlib. Note that distribute_ will also allow us to remove the direct dependency diff --git a/doc/devel/MEP/MEP12.rst b/doc/devel/MEP/MEP12.rst index 1b10ccf446ee..3ab2e74425ee 100644 --- a/doc/devel/MEP/MEP12.rst +++ b/doc/devel/MEP/MEP12.rst @@ -105,7 +105,7 @@ sections described above. "Clean-up" should involve: * PEP8_ clean-ups (running `flake8 - `_, or a similar checker, is + `_, or a similar checker, is highly recommended) * Commented-out code should be removed. * Replace uses of `pylab` interface with `.pyplot` (+ `numpy`, @@ -141,8 +141,8 @@ page instead of the gallery examples. references to that example. For example, the API documentation for :file:`axes.py` and :file:`pyplot.py` may use these examples to generate plots. Use your favorite search tool (e.g., grep, ack, `grin -`_, `pss -`_) to search the matplotlib +`_, `pss +`_) to search the matplotlib package. See `2dc9a46 `_ and `aa6b410 diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index ffac23812e95..7f96f845ba1b 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -1,5 +1,14 @@ .. _pr-guidelines: +.. raw:: html + + + *********************** Pull request guidelines *********************** @@ -23,6 +32,8 @@ Summary for PR authors When making a PR, pay attention to: +.. rst-class:: checklist + * :ref:`Target the master branch `. * Adhere to the :ref:`coding_guidelines`. * Update the :ref:`documentation ` if necessary. @@ -53,6 +64,8 @@ Summary for PR reviewers Content topics: +.. rst-class:: checklist + * Is the feature / bugfix reasonable? * Does the PR conform with the :ref:`coding_guidelines`? * Is the :ref:`documentation ` (docstrings, examples, @@ -60,6 +73,8 @@ Content topics: Organizational topics: +.. rst-class:: checklist + * Make sure all :ref:`automated tests ` pass. * The PR should :ref:`target the master branch `. * Tag with descriptive :ref:`labels `. diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index 41ec56dd0a8d..8b5378b290b7 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -362,5 +362,5 @@ numpy/scipy/scikit-image mailing lists. In addition, announcements should be made on social networks (twitter via the ``@matplotlib`` account, any other via personal accounts). -`NumFOCUS `__ should be contacted for +`NumFOCUS `__ should be contacted for inclusion in their newsletter. diff --git a/doc/index.rst b/doc/index.rst index 5a4fc27e5bf7..fdb15ec9992f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -187,7 +187,7 @@ Open source .. raw:: html - + A Fiscally Sponsored Project of NUMFocus @@ -210,7 +210,7 @@ the NumFOCUS organization or to the `John Hunter Technology Fellowship `_. .. _donating: https://numfocus.org/donate-to-matplotlib -.. _jdh-fellowship: https://www.numfocus.org/programs/john-hunter-technology-fellowship/ +.. _jdh-fellowship: https://numfocus.org/programs/john-hunter-technology-fellowship/ .. _nf: https://numfocus.org The :doc:`Matplotlib license ` is based on the `Python Software diff --git a/doc/missing-references.json b/doc/missing-references.json index 3a33faed67e8..80acaf840b16 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -890,9 +890,6 @@ "cbook.warn_deprecated": [ "doc/api/prev_api_changes/api_changes_3.1.0.rst:766" ], - "cbook.warn_deprecated()": [ - "doc/devel/contributing.rst:330" - ], "cleanup": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.setup:19" ], @@ -989,9 +986,6 @@ "levels": [ "doc/api/prev_api_changes/api_changes_3.0.0.rst:197" ], - "lineprops": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:27" - ], "load_char": [ "doc/gallery/misc/ftface_props.rst:16" ], @@ -1012,9 +1006,6 @@ "make_image": [ "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" ], - "markerprops": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:32" - ], "markevery": [ "doc/users/prev_whats_new/whats_new_1.4.rst:212", "doc/users/prev_whats_new/whats_new_1.4.rst:214", @@ -1434,15 +1425,11 @@ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes.Axes.hexbin:125", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:125" ], - "onselect": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:19" - ], "pillow.Image.save()": [ "doc/users/prev_whats_new/whats_new_3.1.0.rst:131", "doc/users/prev_whats_new/whats_new_3.1.0.rst:135" ], "plot": [ - "doc/tutorials/introductory/customizing.rst:195", "lib/matplotlib/sphinxext/plot_directive.py:docstring of matplotlib.sphinxext.plot_directive:4" ], "plot_include_source": [ @@ -1540,13 +1527,6 @@ "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolFullScreen.disable:4", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolFullScreen.enable:4" ], - "units.AxisInfo": [ - "lib/matplotlib/units.py:docstring of matplotlib.units.ConversionInterface.axisinfo:2", - "lib/matplotlib/units.py:docstring of matplotlib.units.DecimalConverter.axisinfo:2" - ], - "vertex_select_radius": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:37" - ], "w_pad": [ "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure.set_constrained_layout:5" ], diff --git a/doc/sphinxext/github.py b/doc/sphinxext/github.py index be4017e24ed4..0a96ac185f86 100644 --- a/doc/sphinxext/github.py +++ b/doc/sphinxext/github.py @@ -1,4 +1,5 @@ -"""Define text roles for GitHub +""" +Define text roles for GitHub. * ghissue - Issue * ghpull - Pull Request @@ -22,7 +23,8 @@ def make_link_node(rawtext, app, type, slug, options): - """Create a link to a github resource. + """ + Create a link to a github resource. :param rawtext: Text being replaced with link node. :param app: Sphinx application context @@ -53,7 +55,8 @@ def make_link_node(rawtext, app, type, slug, options): def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub issue. + """ + Link to a GitHub issue. Returns 2 part tuple containing list of nodes to insert into the document and a list of system messages. Both are allowed to be @@ -94,7 +97,8 @@ def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub user. + """ + Link to a GitHub user. Returns 2 part tuple containing list of nodes to insert into the document and a list of system messages. Both are allowed to be @@ -115,7 +119,8 @@ def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): def ghcommit_role( name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub commit. + """ + Link to a GitHub commit. Returns 2 part tuple containing list of nodes to insert into the document and a list of system messages. Both are allowed to be @@ -147,7 +152,8 @@ def ghcommit_role( def setup(app): - """Install the plugin. + """ + Install the plugin. :param app: Sphinx application context. """ diff --git a/doc/thirdpartypackages/index.rst b/doc/thirdpartypackages/index.rst index a8208059e40b..cb5f46e54abf 100644 --- a/doc/thirdpartypackages/index.rst +++ b/doc/thirdpartypackages/index.rst @@ -18,19 +18,19 @@ Mapping toolkits Basemap ======= -`Basemap `_ plots data on map projections, with -continental and political boundaries. +`Basemap `_ plots data on map projections, +with continental and political boundaries. .. image:: /_static/basemap_contour1.png :height: 400px Cartopy ======= -`Cartopy `_ builds on top +`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 @@ -68,7 +68,7 @@ to python based on Matplotlib. holoviews ========= `holoviews `_ makes it easier to visualize data -interactively, especially in a `Jupyter notebook `_, by +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 @@ -120,7 +120,7 @@ diagrams. mpl-probscale ============= -`mpl-probscale `_ is a small extension +`mpl-probscale `_ is a small extension that allows Matplotlib users to specify probabilty 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')``. diff --git a/doc/users/history.rst b/doc/users/history.rst index 77c78ed07565..5294382b951e 100644 --- a/doc/users/history.rst +++ b/doc/users/history.rst @@ -11,7 +11,7 @@ Matplotlib is a library for making 2D plots of arrays in `Python the MATLAB graphics commands, it is independent of MATLAB, and can be used in a Pythonic, object oriented way. Although Matplotlib is written primarily in pure Python, it -makes heavy use of `NumPy `_ and other extension +makes heavy use of `NumPy `_ and other extension code to provide good performance even for large arrays. Matplotlib is designed with the philosophy that you should be able to diff --git a/doc/users/next_whats_new/2020-01-24-lateshare.rst b/doc/users/next_whats_new/2020-01-24-lateshare.rst new file mode 100644 index 000000000000..fdb17c85a01a --- /dev/null +++ b/doc/users/next_whats_new/2020-01-24-lateshare.rst @@ -0,0 +1,8 @@ +`.Axes.sharex`, `.Axes.sharey` +------------------------------ +These new methods allow sharing axes *immediately* after creating them. For +example, they can be used to selectively link some axes created all together +using `~.Figure.subplots`. + +Note that they may *not* be used to share axes after any operation (e.g., +drawing) has occurred on them. diff --git a/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst b/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst new file mode 100644 index 000000000000..d340a3111d5a --- /dev/null +++ b/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst @@ -0,0 +1,6 @@ +Cursor text now uses a number of significant digits matching pointing precision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the x/y position displayed by the cursor text would usually include +far more significant digits than the mouse pointing precision (typically one +pixel). This is now fixed for linear scales. diff --git a/doc/users/next_whats_new/2020-04-11-constrainedlayout-not-exp.rst b/doc/users/next_whats_new/2020-04-11-constrainedlayout-not-exp.rst new file mode 100644 index 000000000000..c8068213e14a --- /dev/null +++ b/doc/users/next_whats_new/2020-04-11-constrainedlayout-not-exp.rst @@ -0,0 +1,7 @@ +Constrained layout is no longer marked as experimental +------------------------------------------------------ + +The *constrained_layout* option for figures and gridspecs was introduced +in Matplotlib 2.2, and is no longer considered experimental. For an +overview of the feature, please see +:doc:`/tutorials/intermediate/constrainedlayout_guide`. diff --git a/doc/users/next_whats_new/2020-04-13-AL.rst b/doc/users/next_whats_new/2020-04-13-AL.rst new file mode 100644 index 000000000000..890ceda6bf85 --- /dev/null +++ b/doc/users/next_whats_new/2020-04-13-AL.rst @@ -0,0 +1,7 @@ +`.backend_bases.key_press_handler` and `.backend_bases.button_press_handler` simplifications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These event handlers can now be directly connected to a canvas with +``canvas.mpl_connect("key_press_event", key_press_handler)`` and +``canvas.mpl_connect("button_press_event", button_press_handler)``, rather than +having to write wrapper functions that fill in the (now optional) *canvas* and +*toolbar* parameters. diff --git a/doc/users/next_whats_new/2020-04-14-title-yparam.rst b/doc/users/next_whats_new/2020-04-14-title-yparam.rst new file mode 100644 index 000000000000..53e98f1fe52b --- /dev/null +++ b/doc/users/next_whats_new/2020-04-14-title-yparam.rst @@ -0,0 +1,8 @@ +`~.axes.Axes.set_title` gains a y keyword argument to control auto positioning +------------------------------------------------------------------------------ +`~.axes.Axes.set_title` tries to auto-position the title to avoid any +decorators on the top x-axis. This is not always desirable so now +*y* is an explicit keyword argument of `~.axes.Axes.set_title`. It +defaults to *None* which means to use auto-positioning. If a value is +supplied (i.e. the pre-3.0 default was ``y=1.0``) then auto-positioning is +turned off. This can also be set with the new rcParameter :rc:`axes.titley`. diff --git a/doc/users/prev_whats_new/whats_new_1.0.rst b/doc/users/prev_whats_new/whats_new_1.0.rst index e8bb8f190bff..0dd53cfd0133 100644 --- a/doc/users/prev_whats_new/whats_new_1.0.rst +++ b/doc/users/prev_whats_new/whats_new_1.0.rst @@ -128,7 +128,7 @@ Much improved software carpentry The matplotlib trunk is probably in as good a shape as it has ever been, thanks to improved `software carpentry -`__. We now have a `buildbot +`__. We now have a `buildbot `__ which runs a suite of `nose `__ regression tests on every svn commit, auto-generating a set of images and comparing them against diff --git a/doc/users/prev_whats_new/whats_new_1.3.rst b/doc/users/prev_whats_new/whats_new_1.3.rst index e57e35d230f5..ddc35661dc74 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/users/prev_whats_new/whats_new_1.3.rst @@ -85,7 +85,7 @@ New plotting features `````````````````````````````` To give your plots a sense of authority that they may be missing, Michael Droettboom (inspired by the work of many others in -:ghpull:`1329`) has added an `xkcd-style `__ sketch +:ghpull:`1329`) has added an `xkcd-style `__ sketch plotting mode. To use it, simply call `matplotlib.pyplot.xkcd` before creating your plot. For really fine control, it is also possible to modify each artist's sketch parameters individually with diff --git a/doc/users/prev_whats_new/whats_new_1.4.rst b/doc/users/prev_whats_new/whats_new_1.4.rst index 1b2de3ac15f3..3ba99cd82e6e 100644 --- a/doc/users/prev_whats_new/whats_new_1.4.rst +++ b/doc/users/prev_whats_new/whats_new_1.4.rst @@ -46,7 +46,7 @@ same way as any other matplotlib backend. Because figures require a connection to the IPython notebook server for their interactivity, once the notebook is saved, each figure will be rendered as a static image - thus allowing non-interactive viewing of figures on services such as -`nbviewer `__. +`nbviewer `__. diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/users/prev_whats_new/whats_new_1.5.rst index 444060cc3112..f860fd85e7e1 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -174,7 +174,7 @@ wx backend has been updated ``````````````````````````` The wx backend can now be used with both wxPython classic and -`Phoenix `__. +`Phoenix `__. wxPython classic has to be at least version 2.8.12 and works on Python 2.x. As of May 2015 no official release of wxPython Phoenix is available but a diff --git a/doc/users/prev_whats_new/whats_new_2.2.rst b/doc/users/prev_whats_new/whats_new_2.2.rst index 1153227a8856..ccb43e3c1056 100644 --- a/doc/users/prev_whats_new/whats_new_2.2.rst +++ b/doc/users/prev_whats_new/whats_new_2.2.rst @@ -284,7 +284,7 @@ The binding selection rules are as follows: Thus, to force usage of PGI when both bindings are installed, import it first. .. _PGI: https://pgi.readthedocs.io/en/latest/ -.. _PyGObject: http://pygobject.readthedocs.io/en/latest/# +.. _PyGObject: https://pygobject.readthedocs.io/en/latest/ diff --git a/examples/axisartist/demo_floating_axis.py b/examples/axisartist/demo_floating_axis.py index fe7bdc82f570..276fad0a99e3 100644 --- a/examples/axisartist/demo_floating_axis.py +++ b/examples/axisartist/demo_floating_axis.py @@ -19,8 +19,7 @@ def curvelinear_test2(fig): - """Polar projection, but in a rectangular box. - """ + """Polar projection, but in a rectangular box.""" # see demo_curvelinear_grid.py for details tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() diff --git a/examples/images_contours_and_fields/contour_demo.py b/examples/images_contours_and_fields/contour_demo.py index 763b0789a870..2c9881c83fe1 100644 --- a/examples/images_contours_and_fields/contour_demo.py +++ b/examples/images_contours_and_fields/contour_demo.py @@ -31,7 +31,7 @@ fig, ax = plt.subplots() CS = ax.contour(X, Y, Z) -ax.clabel(CS, inline=1, fontsize=10) +ax.clabel(CS, inline=True, fontsize=10) ax.set_title('Simplest default with labels') ############################################################################### @@ -43,7 +43,7 @@ CS = ax.contour(X, Y, Z) manual_locations = [ (-1, -1.4), (-0.62, -0.7), (-2, 0.5), (1.7, 1.2), (2.0, 1.4), (2.4, 1.7)] -ax.clabel(CS, inline=1, fontsize=10, manual=manual_locations) +ax.clabel(CS, inline=True, fontsize=10, manual=manual_locations) ax.set_title('labels at selected locations') ############################################################################### @@ -51,7 +51,7 @@ fig, ax = plt.subplots() CS = ax.contour(X, Y, Z, 6, colors='k') # Negative contours default to dashed. -ax.clabel(CS, fontsize=9, inline=1) +ax.clabel(CS, fontsize=9, inline=True) ax.set_title('Single color - negative contours dashed') ############################################################################### @@ -60,7 +60,7 @@ matplotlib.rcParams['contour.negative_linestyle'] = 'solid' fig, ax = plt.subplots() CS = ax.contour(X, Y, Z, 6, colors='k') # Negative contours default to dashed. -ax.clabel(CS, fontsize=9, inline=1) +ax.clabel(CS, fontsize=9, inline=True) ax.set_title('Single color - negative contours solid') ############################################################################### @@ -71,7 +71,7 @@ linewidths=np.arange(.5, 4, .5), colors=('r', 'green', 'blue', (1, 1, 0), '#afeeee', '0.5'), ) -ax.clabel(CS, fontsize=9, inline=1) +ax.clabel(CS, fontsize=9, inline=True) ax.set_title('Crazy lines') ############################################################################### @@ -90,7 +90,7 @@ plt.setp(zc, linewidth=4) ax.clabel(CS, levels[1::2], # label every second level - inline=1, fmt='%1.1f', fontsize=14) + inline=True, fmt='%1.1f', fontsize=14) # make a colorbar for the contour lines CB = fig.colorbar(CS, shrink=0.8) diff --git a/examples/images_contours_and_fields/multi_image.py b/examples/images_contours_and_fields/multi_image.py index e8df23d1d81e..aa9067e55d06 100644 --- a/examples/images_contours_and_fields/multi_image.py +++ b/examples/images_contours_and_fields/multi_image.py @@ -13,7 +13,6 @@ np.random.seed(19680801) Nr = 3 Nc = 2 -cmap = "cool" fig, axs = plt.subplots(Nr, Nc) fig.suptitle('Multiple images') @@ -22,8 +21,8 @@ for i in range(Nr): for j in range(Nc): # Generate data with a range that varies from one plot to the next. - data = ((1 + i + j) / 10) * np.random.rand(10, 20) * 1e-6 - images.append(axs[i, j].imshow(data, cmap=cmap)) + data = ((1 + i + j) / 10) * np.random.rand(10, 20) + images.append(axs[i, j].imshow(data)) axs[i, j].label_outer() # Find the min and max of all colors for use in setting the color scale. diff --git a/examples/images_contours_and_fields/pcolormesh_grids.py b/examples/images_contours_and_fields/pcolormesh_grids.py index 9b6457108b26..b680cfba7a19 100644 --- a/examples/images_contours_and_fields/pcolormesh_grids.py +++ b/examples/images_contours_and_fields/pcolormesh_grids.py @@ -27,7 +27,7 @@ # and if the grid is one larger than the data in each dimension, i.e. has shape # *(M+1, N+1)*. In that case *X* and *Y* specify the corners of quadrilaterals # that are colored with the values in *Z*. Here we specify the edges of the -# *(3, 5)* quadrilaterals with *X* and *Y* that are *(4, 6)*. +# *(3, 5)* quadrilaterals with *X* and *Y* that are *(4, 6)*. nrows = 3 ncols = 5 diff --git a/examples/lines_bars_and_markers/fill_between_demo.py b/examples/lines_bars_and_markers/fill_between_demo.py index 6b9566033472..8f7e7c11d024 100644 --- a/examples/lines_bars_and_markers/fill_between_demo.py +++ b/examples/lines_bars_and_markers/fill_between_demo.py @@ -14,8 +14,8 @@ # # Basic usage # ----------- -# The parameters *y1* and *y2* can be a scalar, indicating a horizontal -# boundary a the given y-values. If only *y1* is given, *y2* defaults to 0. +# The parameters *y1* and *y2* can be scalars, indicating a horizontal +# boundary at the given y-values. If only *y1* is given, *y2* defaults to 0. x = np.arange(0.0, 2, 0.01) y1 = np.sin(2 * np.pi * x) diff --git a/examples/lines_bars_and_markers/filled_step.py b/examples/lines_bars_and_markers/filled_step.py index a156665b0d49..cdbb2efdedfd 100644 --- a/examples/lines_bars_and_markers/filled_step.py +++ b/examples/lines_bars_and_markers/filled_step.py @@ -92,7 +92,7 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, sty_cycle : Cycler or operable of dict Style to apply to each set - bottoms : array, optional, default: 0 + bottoms : array, default: 0 The initial positions of the bottoms. hist_func : callable, optional diff --git a/examples/misc/demo_agg_filter.py b/examples/misc/demo_agg_filter.py index 80dde60de314..94379b214946 100644 --- a/examples/misc/demo_agg_filter.py +++ b/examples/misc/demo_agg_filter.py @@ -170,7 +170,7 @@ def filtered_text(ax): # contour label cl = ax.clabel(CS, levels[1::2], # label every second level - inline=1, + inline=True, fmt='%1.1f', fontsize=11) diff --git a/examples/misc/image_thumbnail_sgskip.py b/examples/misc/image_thumbnail_sgskip.py index 8bbc8626a55c..97a9e9a627ea 100644 --- a/examples/misc/image_thumbnail_sgskip.py +++ b/examples/misc/image_thumbnail_sgskip.py @@ -7,7 +7,7 @@ Matplotlib relies on Pillow_ for reading images, and thus supports all formats supported by Pillow. -.. _Pillow: http://python-pillow.org/ +.. _Pillow: https://python-pillow.org/ """ from argparse import ArgumentParser diff --git a/examples/mplot3d/contour3d.py b/examples/mplot3d/contour3d.py index a740c30376c1..8ae9deb54981 100644 --- a/examples/mplot3d/contour3d.py +++ b/examples/mplot3d/contour3d.py @@ -18,6 +18,6 @@ # Plot contour curves cset = ax.contour(X, Y, Z, cmap=cm.coolwarm) -ax.clabel(cset, fontsize=9, inline=1) +ax.clabel(cset, fontsize=9, inline=True) plt.show() diff --git a/examples/mplot3d/contour3d_2.py b/examples/mplot3d/contour3d_2.py index b77b2bdc0557..686fc4aa39f0 100644 --- a/examples/mplot3d/contour3d_2.py +++ b/examples/mplot3d/contour3d_2.py @@ -17,6 +17,6 @@ cset = ax.contour(X, Y, Z, extend3d=True, cmap=cm.coolwarm) -ax.clabel(cset, fontsize=9, inline=1) +ax.clabel(cset, fontsize=9, inline=True) plt.show() diff --git a/examples/mplot3d/contourf3d.py b/examples/mplot3d/contourf3d.py index 81c058905930..1fcfe5c634bf 100644 --- a/examples/mplot3d/contourf3d.py +++ b/examples/mplot3d/contourf3d.py @@ -20,6 +20,6 @@ cset = ax.contourf(X, Y, Z, cmap=cm.coolwarm) -ax.clabel(cset, fontsize=9, inline=1) +ax.clabel(cset, fontsize=9, inline=True) plt.show() diff --git a/examples/showcase/anatomy.py b/examples/showcase/anatomy.py index b29f99f0dd9d..bd1cee43627c 100644 --- a/examples/showcase/anatomy.py +++ b/examples/showcase/anatomy.py @@ -137,7 +137,7 @@ def text(x, y, text): connectionstyle="arc3", color=color)) -ax.text(4.0, -0.4, "Made with http://matplotlib.org", +ax.text(4.0, -0.4, "Made with https://matplotlib.org", fontsize=10, ha="right", color='.5') plt.show() diff --git a/examples/showcase/mandelbrot.py b/examples/showcase/mandelbrot.py index 67cdfbfb4e7e..6f979104cefc 100644 --- a/examples/showcase/mandelbrot.py +++ b/examples/showcase/mandelbrot.py @@ -66,7 +66,7 @@ def mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon=2.0): # Some advertisement for matplotlib year = time.strftime("%Y") text = ("The Mandelbrot fractal set\n" - "Rendered with matplotlib %s, %s - http://matplotlib.org" + "Rendered with matplotlib %s, %s - https://matplotlib.org" % (matplotlib.__version__, year)) ax.text(xmin+.025, ymin+.025, text, color="white", fontsize=12, alpha=0.5) diff --git a/examples/specialty_plots/radar_chart.py b/examples/specialty_plots/radar_chart.py index ed30232e4898..42358c766d01 100644 --- a/examples/specialty_plots/radar_chart.py +++ b/examples/specialty_plots/radar_chart.py @@ -26,7 +26,8 @@ def radar_factory(num_vars, frame='circle'): - """Create a radar chart with `num_vars` axes. + """ + Create a radar chart with `num_vars` axes. This function creates a RadarAxes projection and registers it. diff --git a/examples/statistics/boxplot_vs_violin.py b/examples/statistics/boxplot_vs_violin.py index 73206fef6abb..37fe844c0617 100644 --- a/examples/statistics/boxplot_vs_violin.py +++ b/examples/statistics/boxplot_vs_violin.py @@ -17,7 +17,7 @@ Violin plots require matplotlib >= 1.4. For more information on violin plots, the scikit-learn docs have a great -section: http://scikit-learn.org/stable/modules/density.html +section: https://scikit-learn.org/stable/modules/density.html """ import matplotlib.pyplot as plt diff --git a/examples/statistics/confidence_ellipse.py b/examples/statistics/confidence_ellipse.py index 2a73099b70db..e7e6f54eaf1c 100644 --- a/examples/statistics/confidence_ellipse.py +++ b/examples/statistics/confidence_ellipse.py @@ -53,13 +53,12 @@ def confidence_ellipse(x, y, ax, n_std=3.0, facecolor='none', **kwargs): n_std : float The number of standard deviations to determine the ellipse's radiuses. + **kwargs + Forwarded to `~matplotlib.patches.Ellipse` + Returns ------- matplotlib.patches.Ellipse - - Other parameters - ---------------- - kwargs : `~matplotlib.patches.Patch` properties """ if x.size != y.size: raise ValueError("x and y must be the same size") diff --git a/examples/statistics/customized_violin.py b/examples/statistics/customized_violin.py index 86c35893e370..3b6dac6082c3 100644 --- a/examples/statistics/customized_violin.py +++ b/examples/statistics/customized_violin.py @@ -11,7 +11,7 @@ of the violins are modified. For more information on violin plots, the scikit-learn docs have a great -section: http://scikit-learn.org/stable/modules/density.html +section: https://scikit-learn.org/stable/modules/density.html """ import matplotlib.pyplot as plt diff --git a/examples/statistics/violinplot.py b/examples/statistics/violinplot.py index 319ab9f42a92..1e1911d262cf 100644 --- a/examples/statistics/violinplot.py +++ b/examples/statistics/violinplot.py @@ -13,7 +13,7 @@ and how to modify the band-width of the KDE (``bw_method``). For more information on violin plots and KDE, the scikit-learn docs -have a great section: http://scikit-learn.org/stable/modules/density.html +have a great section: https://scikit-learn.org/stable/modules/density.html """ import numpy as np diff --git a/examples/style_sheets/style_sheets_reference.py b/examples/style_sheets/style_sheets_reference.py index bf2ed08f93c5..e1566cc501f3 100644 --- a/examples/style_sheets/style_sheets_reference.py +++ b/examples/style_sheets/style_sheets_reference.py @@ -17,8 +17,7 @@ def plot_scatter(ax, prng, nb_samples=100): - """Scatter plot. - """ + """Scatter plot.""" for mu, sigma, marker in [(-.5, 0.75, 'o'), (0.75, 1., 's')]: x, y = prng.normal(loc=mu, scale=sigma, size=(2, nb_samples)) ax.plot(x, y, ls='none', marker=marker) @@ -28,8 +27,7 @@ def plot_scatter(ax, prng, nb_samples=100): def plot_colored_sinusoidal_lines(ax): - """Plot sinusoidal lines with colors following the style color cycle. - """ + """Plot sinusoidal lines with colors following the style color cycle.""" L = 2 * np.pi x = np.linspace(0, L) nb_colors = len(plt.rcParams['axes.prop_cycle']) @@ -41,8 +39,7 @@ def plot_colored_sinusoidal_lines(ax): def plot_bar_graphs(ax, prng, min_value=5, max_value=25, nb_samples=5): - """Plot two bar graphs side by side, with letters as x-tick labels. - """ + """Plot two bar graphs side by side, with letters as x-tick labels.""" x = np.arange(nb_samples) ya, yb = prng.randint(min_value, max_value, size=(2, nb_samples)) width = 0.25 @@ -54,7 +51,8 @@ def plot_bar_graphs(ax, prng, min_value=5, max_value=25, nb_samples=5): def plot_colored_circles(ax, prng, nb_samples=15): - """Plot circle patches. + """ + Plot circle patches. NB: draws a fixed amount of samples, rather than using the length of the color cycle, because different styles may have different numbers @@ -72,8 +70,7 @@ def plot_colored_circles(ax, prng, nb_samples=15): def plot_image_and_patch(ax, prng, size=(20, 20)): - """Plot an image with random values and superimpose a circular patch. - """ + """Plot an image with random values and superimpose a circular patch.""" values = prng.random_sample(size=size) ax.imshow(values, interpolation='none') c = plt.Circle((5, 5), radius=5, label='patch') @@ -84,8 +81,7 @@ def plot_image_and_patch(ax, prng, size=(20, 20)): def plot_histograms(ax, prng, nb_samples=10000): - """Plot 4 histograms and a text annotation. - """ + """Plot 4 histograms and a text annotation.""" params = ((10, 10), (4, 12), (50, 12), (6, 55)) for a, b in params: values = prng.beta(a, b, size=nb_samples) @@ -104,8 +100,7 @@ def plot_histograms(ax, prng, nb_samples=10000): def plot_figure(style_label=""): - """Setup and plot the demonstration figure with a given style. - """ + """Setup and plot the demonstration figure with a given style.""" # Use a dedicated RandomState instance to draw the same "random" values # across the different figures. prng = np.random.RandomState(96917002) diff --git a/examples/subplots_axes_and_figures/subplots_demo.py b/examples/subplots_axes_and_figures/subplots_demo.py index 27a6d1a95c67..e0bf7fe68521 100644 --- a/examples/subplots_axes_and_figures/subplots_demo.py +++ b/examples/subplots_axes_and_figures/subplots_demo.py @@ -94,7 +94,7 @@ ax.label_outer() ############################################################################### -# You can use tuple-unpacking also in 2D to assign all subplots to dedicated +# You can use tuple-unpacking also in 2D to assign all subplots to dedicated # variables: fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) @@ -176,6 +176,23 @@ for ax in axs.flat: ax.label_outer() +############################################################################### +# If you want a more complex sharing structure, you can first create the +# grid of axes with no sharing, and then call `.axes.Axes.sharex` or +# `.axes.Axes.sharey` to add sharing info a posteriori. + +fig, axs = plt.subplots(2, 2) +axs[0, 0].plot(x, y) +axs[0, 0].set_title("main") +axs[1, 0].plot(x, y**2) +axs[1, 0].set_title("shares x with main") +axs[1, 0].sharex(axs[0, 0]) +axs[0, 1].plot(x + 1, y + 1) +axs[0, 1].set_title("unrelated") +axs[1, 1].plot(x + 2, y + 2) +axs[1, 1].set_title("also unrelated") +fig.tight_layout() + ############################################################################### # Polar axes # """""""""" diff --git a/examples/text_labels_and_annotations/arrow_demo.py b/examples/text_labels_and_annotations/arrow_demo.py index 0b8ccec4bcab..9e678249d1a1 100644 --- a/examples/text_labels_and_annotations/arrow_demo.py +++ b/examples/text_labels_and_annotations/arrow_demo.py @@ -29,7 +29,8 @@ def make_arrow_plot(data, size=4, display='length', shape='right', head_starts_at_zero=True, rate_labels=lettered_bases_to_rates, **kwargs): - """Makes an arrow plot. + """ + Makes an arrow plot. Parameters ---------- diff --git a/examples/text_labels_and_annotations/custom_legends.py b/examples/text_labels_and_annotations/custom_legends.py index 28b73fe65da2..e9f840fe768f 100644 --- a/examples/text_labels_and_annotations/custom_legends.py +++ b/examples/text_labels_and_annotations/custom_legends.py @@ -33,10 +33,10 @@ fig, ax = plt.subplots() lines = ax.plot(data) -ax.legend(lines) +ax.legend() ############################################################################## -# Note that one legend item per line was created. +# Note that no legend entries were created. # In this case, we can compose a legend using Matplotlib objects that aren't # explicitly tied to the data that was plotted. For example: diff --git a/examples/text_labels_and_annotations/demo_annotation_box.py b/examples/text_labels_and_annotations/demo_annotation_box.py index 9fa69dd77623..cf94874509bf 100644 --- a/examples/text_labels_and_annotations/demo_annotation_box.py +++ b/examples/text_labels_and_annotations/demo_annotation_box.py @@ -1,4 +1,5 @@ -"""=================== +""" +=================== Demo Annotation Box =================== diff --git a/examples/text_labels_and_annotations/titles_demo.py b/examples/text_labels_and_annotations/titles_demo.py index 5fb5544e9dc4..6d77774246ea 100644 --- a/examples/text_labels_and_annotations/titles_demo.py +++ b/examples/text_labels_and_annotations/titles_demo.py @@ -1,9 +1,9 @@ """ -=========== -Titles Demo -=========== +================= +Title positioning +================= -matplotlib can display plot titles centered, flush with the left side of +Matplotlib can display plot titles centered, flush with the left side of a set of axes, and flush with the right side of a set of axes. """ @@ -16,3 +16,44 @@ plt.title('Right Title', loc='right') plt.show() + +########################################################################### +# The vertical position is automatically chosen to avoid decorations +# (i.e. labels and ticks) on the topmost x-axis: + +fig, axs = plt.subplots(1, 2, constrained_layout=True) + +ax = axs[0] +ax.plot(range(10)) +ax.xaxis.set_label_position('top') +ax.set_xlabel('X-label') +ax.set_title('Center Title') + +ax = axs[1] +ax.plot(range(10)) +ax.xaxis.set_label_position('top') +ax.xaxis.tick_top() +ax.set_xlabel('X-label') +ax.set_title('Center Title') +plt.show() + +########################################################################### +# Automatic positioning can be turned off by manually specifying the +# *y* kwarg for the title or setting :rc:`axes.titley` in the rcParams. + +fig, axs = plt.subplots(1, 2, constrained_layout=True) + +ax = axs[0] +ax.plot(range(10)) +ax.xaxis.set_label_position('top') +ax.set_xlabel('X-label') +ax.set_title('Manual y', y=1.0, pad=-14) + +plt.rcParams['axes.titley'] = 1.0 # y is in axes-relative co-ordinates. +plt.rcParams['axes.titlepad'] = -14 # pad is in points... +ax = axs[1] +ax.plot(range(10)) +ax.set_xlabel('X-label') +ax.set_title('rcParam y') + +plt.show() diff --git a/examples/user_interfaces/embedding_in_tk_sgskip.py b/examples/user_interfaces/embedding_in_tk_sgskip.py index 2d9e246f73ef..6a6cc1605f01 100644 --- a/examples/user_interfaces/embedding_in_tk_sgskip.py +++ b/examples/user_interfaces/embedding_in_tk_sgskip.py @@ -31,12 +31,9 @@ toolbar.update() -def on_key_press(event): - print("you pressed {}".format(event.key)) - key_press_handler(event, canvas, toolbar) - - -canvas.mpl_connect("key_press_event", on_key_press) +canvas.mpl_connect( + "key_press_event", lambda event: print(f"you pressed {event.key}")) +canvas.mpl_connect("key_press_event", key_press_handler) button = tkinter.Button(master=root, text="Quit", command=root.quit) diff --git a/examples/userdemo/demo_gridspec06.py b/examples/userdemo/demo_gridspec06.py index 7bf7886ab0bd..507c497b4486 100644 --- a/examples/userdemo/demo_gridspec06.py +++ b/examples/userdemo/demo_gridspec06.py @@ -7,9 +7,7 @@ """ import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec import numpy as np -from itertools import product def squiggle_xy(a, b, c, d): @@ -18,35 +16,23 @@ def squiggle_xy(a, b, c, d): fig = plt.figure(figsize=(8, 8)) - -# gridspec inside gridspec -outer_grid = gridspec.GridSpec(4, 4, wspace=0.0, hspace=0.0) - -for i in range(16): - inner_grid = gridspec.GridSpecFromSubplotSpec( - 3, 3, subplot_spec=outer_grid[i], wspace=0.0, hspace=0.0) - a = i // 4 + 1 - b = i % 4 + 1 - for j, (c, d) in enumerate(product(range(1, 4), repeat=2)): - ax = fig.add_subplot(inner_grid[j]) - ax.plot(*squiggle_xy(a, b, c, d)) - ax.set_xticks([]) - ax.set_yticks([]) - fig.add_subplot(ax) - -all_axes = fig.get_axes() +outer_grid = fig.add_gridspec(4, 4, wspace=0, hspace=0) + +for a in range(4): + for b in range(4): + # gridspec inside gridspec + inner_grid = outer_grid[a, b].subgridspec(3, 3, wspace=0, hspace=0) + for c in range(3): + for d in range(3): + ax = fig.add_subplot(inner_grid[c, d]) + ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1)) + ax.set(xticks=[], yticks=[]) # show only the outside spines -for ax in all_axes: - for sp in ax.spines.values(): - sp.set_visible(False) - if ax.is_first_row(): - ax.spines['top'].set_visible(True) - if ax.is_last_row(): - ax.spines['bottom'].set_visible(True) - if ax.is_first_col(): - ax.spines['left'].set_visible(True) - if ax.is_last_col(): - ax.spines['right'].set_visible(True) +for ax in fig.get_axes(): + ax.spines['top'].set_visible(ax.is_first_row()) + ax.spines['bottom'].set_visible(ax.is_last_row()) + ax.spines['left'].set_visible(ax.is_first_col()) + ax.spines['right'].set_visible(ax.is_last_col()) plt.show() diff --git a/examples/widgets/lasso_selector_demo_sgskip.py b/examples/widgets/lasso_selector_demo_sgskip.py index e34a06a29b2a..b158f0ac78cc 100644 --- a/examples/widgets/lasso_selector_demo_sgskip.py +++ b/examples/widgets/lasso_selector_demo_sgskip.py @@ -18,7 +18,8 @@ class SelectFromCollection: - """Select indices from a matplotlib collection using `LassoSelector`. + """ + Select indices from a matplotlib collection using `LassoSelector`. Selected indices are saved in the `ind` attribute. This tool fades out the points that are not part of the selection (i.e., reduces their alpha diff --git a/examples/widgets/textbox.py b/examples/widgets/textbox.py index 96d1133686d9..8154126b6133 100644 --- a/examples/widgets/textbox.py +++ b/examples/widgets/textbox.py @@ -16,12 +16,13 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import TextBox + + fig, ax = plt.subplots() -plt.subplots_adjust(bottom=0.2) +fig.subplots_adjust(bottom=0.2) + t = np.arange(-2.0, 2.0, 0.001) -s = t ** 2 -initial_text = "t ** 2" -l, = plt.plot(t, s, lw=2) # make a plot for the math expression "t ** 2" +l, = ax.plot(t, np.zeros_like(t), lw=2) def submit(expression): @@ -33,12 +34,15 @@ def submit(expression): """ ydata = eval(expression) l.set_ydata(ydata) - ax.set_ylim(np.min(ydata), np.max(ydata)) + ax.relim() + ax.autoscale_view() plt.draw() -axbox = plt.axes([0.1, 0.05, 0.8, 0.075]) -text_box = TextBox(axbox, 'Evaluate', initial=initial_text) + +axbox = fig.add_axes([0.1, 0.05, 0.8, 0.075]) +text_box = TextBox(axbox, "Evaluate") text_box.on_submit(submit) +text_box.set_val("t ** 2") # Trigger `submit` with the initial string. plt.show() diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 0e8ff7938006..02179b8809cb 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1,5 +1,5 @@ """ -This is an object-oriented plotting library. +An object-oriented plotting library. A procedural interface is provided by the companion pyplot module, which may be imported directly, e.g.:: @@ -192,7 +192,7 @@ def _ensure_handler(): def set_loglevel(level): """ - Sets the Matplotlib's root logger and root logger handler level, creating + Set Matplotlib's root logger and root logger handler level, creating the handler if it does not exist yet. Typically, one should call ``set_loglevel("info")`` or @@ -443,18 +443,19 @@ def _create_tmp_config_or_cache_dir(): def _get_xdg_config_dir(): """ - Return the XDG configuration directory, according to the `XDG - base directory spec - `_. + Return the XDG configuration directory, according to the XDG base + directory spec: + + https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html """ return os.environ.get('XDG_CONFIG_HOME') or str(Path.home() / ".config") def _get_xdg_cache_dir(): """ - Return the XDG cache directory, according to the `XDG - base directory spec - `_. + Return the XDG cache directory, according to the XDG base directory spec: + + https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html """ return os.environ.get('XDG_CACHE_HOME') or str(Path.home() / ".cache") diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index bb20713700dc..c7f5682bdc1f 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -1,18 +1,16 @@ """ -This module provides the routine to adjust subplot layouts so that there are -no overlapping axes or axes decorations. All axes decorations are dealt with -(labels, ticks, titles, ticklabels) and some dependent artists are also dealt -with (colorbar, suptitle, legend). - -Layout is done via :meth:`~matplotlib.gridspec`, with one constraint per -gridspec, so it is possible to have overlapping axes if the gridspecs -overlap (i.e. using :meth:`~matplotlib.gridspec.GridSpecFromSubplotSpec`). -Axes placed using ``figure.subplots()`` or ``figure.add_subplots()`` will -participate in the layout. Axes manually placed via ``figure.add_axes()`` -will not. +Adjust subplot layouts so that there are no overlapping axes or axes +decorations. All axes decorations are dealt with (labels, ticks, titles, +ticklabels) and some dependent artists are also dealt with (colorbar, suptitle, +legend). -See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide` +Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec, +so it is possible to have overlapping axes if the gridspecs overlap (i.e. +using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using +``figure.subplots()`` or ``figure.add_subplots()`` will participate in the +layout. Axes manually placed via ``figure.add_axes()`` will not. +See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide` """ # Development Notes: diff --git a/lib/matplotlib/afm.py b/lib/matplotlib/afm.py index dc33f0300766..dc7bbeb19672 100644 --- a/lib/matplotlib/afm.py +++ b/lib/matplotlib/afm.py @@ -1,14 +1,16 @@ """ -This is a python interface to Adobe Font Metrics Files. Although a -number of other python implementations exist, and may be more complete -than this, it was decided not to go with them because they were either: +A python interface to Adobe Font Metrics Files. + +Although a number of other python implementations exist, and may be more +complete than this, it was decided not to go with them because they were +either: 1) copyrighted or used a non-BSD compatible license 2) had too many dependencies and a free standing lib was needed 3) did more than needed and it was easier to write afresh rather than figure out how to get just what was needed. -It is pretty easy to use, and requires only built-in python libs: +It is pretty easy to use, and has no external depedencies: >>> import matplotlib as mpl >>> from pathlib import Path @@ -85,7 +87,7 @@ def _to_bool(s): def _parse_header(fh): """ - Reads the font metrics header (up to the char metrics) and returns + Read the font metrics header (up to the char metrics) and returns a dictionary mapping *key* to *val*. *val* will be converted to the appropriate python type as necessary; e.g.: @@ -99,7 +101,6 @@ def _parse_header(fh): ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition, UnderlineThickness, Version, Notice, EncodingScheme, CapHeight, XHeight, Ascender, Descender, StartCharMetrics - """ header_converters = { b'StartFontMetrics': _to_float, @@ -128,8 +129,7 @@ def _parse_header(fh): b'StartCharMetrics': _to_int, b'CharacterSet': _to_str, b'Characters': _to_int, - } - + } d = {} first_line = True for line in fh: diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 05fc78d574b9..646b266a1c7f 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -93,9 +93,9 @@ def correct_roundoff(x, dpi, n): return correct_roundoff(wnew, dpi, n), correct_roundoff(hnew, dpi, n) -# A registry for available MovieWriter classes class MovieWriterRegistry: """Registry of available writer classes by human readable name.""" + def __init__(self): self._registered = dict() @@ -104,7 +104,8 @@ def set_dirty(self): """Sets a flag to re-setup the writers.""" def register(self, name): - """Decorator for registering a class under a name. + """ + Decorator for registering a class under a name. Example use:: @@ -200,7 +201,7 @@ def __init__(self, fps=5, metadata=None, codec=None, bitrate=None): @abc.abstractmethod def setup(self, fig, outfile, dpi=None): """ - Perform setup for writing the movie file. + Setup for writing the movie file. Parameters ---------- @@ -208,7 +209,7 @@ def setup(self, fig, outfile, dpi=None): The figure object that contains the information for frames. outfile : str The filename of the resulting movie file. - dpi : float, optional, default: ``fig.dpi`` + dpi : float, default: ``fig.dpi`` The DPI (or resolution) for the file. This controls the size in pixels of the resulting movie file. """ @@ -424,7 +425,7 @@ def __init__(self, *args, **kwargs): def setup(self, fig, outfile, dpi=None, frame_prefix=None, clear_temp=True): """ - Perform setup for writing the movie file. + Setup for writing the movie file. Parameters ---------- @@ -564,7 +565,8 @@ def finish(self): # Base class of ffmpeg information. Has the config keys and the common set # of arguments that controls the *output* side of things. class FFMpegBase: - """Mixin class for FFMpeg output. + """ + Mixin class for FFMpeg output. To be useful this must be multiply-inherited from with a `MovieWriterBase` sub-class. @@ -606,7 +608,8 @@ def isAvailable(cls): # Combine FFMpeg options with pipe-based writing @writers.register('ffmpeg') class FFMpegWriter(FFMpegBase, MovieWriter): - """Pipe-based ffmpeg writer. + """ + Pipe-based ffmpeg writer. Frames are streamed directly to ffmpeg via a pipe and written in a single pass. @@ -887,12 +890,10 @@ def finish(self): class Animation: """ - This class wraps the creation of an animation using matplotlib. - - It is only a base class which should be subclassed to provide - needed behavior. + A base class for Animations. - This class is not typically used directly. + This class is not usable as is, and should be subclassed to provide needed + behavior. Parameters ---------- @@ -989,7 +990,7 @@ class to use, such as 'ffmpeg'. Controls the dots per inch for the movie frames. Together with the figure's size in inches, this controls the size of the movie. - codec : str, optional, default: :rc:`animation.codec`. + codec : str, default: :rc:`animation.codec`. The video codec to use. Not all codecs are supported by a given `MovieWriter`. diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 035b87fa6251..41e890352fbc 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -669,7 +669,8 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): self.stale = True def set_path_effects(self, path_effects): - """Set the path effects. + """ + Set the path effects. Parameters ---------- @@ -883,7 +884,8 @@ def get_agg_filter(self): return self._agg_filter def set_agg_filter(self, filter_func): - """Set the agg filter. + """ + Set the agg filter. Parameters ---------- @@ -980,7 +982,12 @@ def update(self, props): ret = [] with cbook._setattr_cm(self, eventson=False): for k, v in props.items(): - k = k.lower() + if k != k.lower(): + cbook.warn_deprecated( + "3.3", message="Case-insensitive properties were " + "deprecated in %(since)s and support will be removed " + "%(removal)s") + k = k.lower() # White list attributes we want to be able to update through # art.update, art.set, setp. if k == "axes": diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3e618964b0c3..7e3223b046fc 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -114,7 +114,8 @@ def get_title(self, loc="center"): title = cbook._check_getitem(titles, loc=loc.lower()) return title.get_text() - def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs): + def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None, + **kwargs): """ Set a title for the axes. @@ -140,6 +141,11 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs): loc : {'center', 'left', 'right'}, default: :rc:`axes.titlelocation` Which title to set. + y : float, default: :rc:`axes.titley` + Vertical axes loation for the title (1.0 is the top). If + None (the default), y is determined automatically to avoid + decorators on the axes. + pad : float, default: :rc:`axes.titlepad` The offset of the title from the top of the axes, in points. @@ -157,6 +163,14 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs): if loc is None: loc = rcParams['axes.titlelocation'] + if y is None: + y = rcParams['axes.titley'] + if y is None: + y = 1.0 + else: + self._autotitlepos = False + kwargs['y'] = y + titles = {'left': self._left_title, 'center': self.title, 'right': self._right_title} @@ -2787,7 +2801,7 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, ----- .. seealso:: The MATLAB function - `stem `_ + `stem `_ which inspired this method. """ if not 1 <= len(args) <= 5: @@ -5390,7 +5404,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, If *interpolation* is 'none', then no interpolation is performed on the Agg, ps, pdf and svg backends. Other backends will fall back - to 'nearest'. Note that most SVG renders perform interpolation at + to 'nearest'. Note that most SVG renderers perform interpolation at rendering and that the default interpolation method they implement may differ. @@ -6833,12 +6847,12 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, weights : array-like, shape (n, ), optional An array of values w_i weighing each sample (x_i, y_i). - cmin : scalar, optional, default: None + cmin : scalar, default: None All bins that has count less than cmin will not be displayed (set to NaN before passing to imshow) and these count values in the return value count histogram will also be set to nan upon return. - cmax : scalar, optional, default: None + cmax : scalar, default: None All bins that has count more than cmax will not be displayed (set to NaN before passing to imshow) and these count values in the return value count histogram will also be set to nan upon return. @@ -7834,7 +7848,8 @@ def _kde_method(X, coords): def violin(self, vpstats, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False): - """Drawing function for violin plots. + """ + Drawing function for violin plots. Draw a violin plot for each column of *vpstats*. Each filled area extends to represent the entire data range, with optional lines at the diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6d8eb4f8ebc6..6fdf67970abe 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -455,8 +455,8 @@ def __init__(self, fig, rect, The x or y `~.matplotlib.axis` is shared with the x or y axis in the input `~.axes.Axes`. - frameon : bool, optional - True means that the axes frame is visible. + frameon : bool, default: True + Whether the axes frame is visible. box_aspect : None, or a number, optional Sets the aspect of the axes box. See `~.axes.Axes.set_box_aspect` @@ -487,16 +487,8 @@ def __init__(self, fig, rect, self._anchor = 'C' self._stale_viewlim_x = False self._stale_viewlim_y = False - self._sharex = sharex - self._sharey = sharey - if sharex is not None: - if not isinstance(sharex, _AxesBase): - raise TypeError('sharex must be an axes, not a bool') - self._shared_x_axes.join(self, sharex) - if sharey is not None: - if not isinstance(sharey, _AxesBase): - raise TypeError('sharey must be an axes, not a bool') - self._shared_y_axes.join(self, sharey) + self._sharex = None + self._sharey = None self.set_label(label) self.set_figure(fig) self.set_box_aspect(box_aspect) @@ -515,6 +507,11 @@ def __init__(self, fig, rect, self._rasterization_zorder = None self.cla() + if sharex is not None: + self.sharex(sharex) + if sharey is not None: + self.sharey(sharey) + # funcs used to format x and y - fall back on major formatters self.fmt_xdata = None self.fmt_ydata = None @@ -909,7 +906,7 @@ def set_position(self, pos, which='both'): pos : [left, bottom, width, height] or `~matplotlib.transforms.Bbox` The new position of the in `.Figure` coordinates. - which : {'both', 'active', 'original'}, optional + which : {'both', 'active', 'original'}, default: 'both' Determines which position variables to change. """ @@ -1008,6 +1005,44 @@ def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): return OrderedDict((side, mspines.Spine.linear_spine(self, side)) for side in ['left', 'right', 'bottom', 'top']) + def sharex(self, other): + """ + Share the x-axis with *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. + """ + cbook._check_isinstance(_AxesBase, other=other) + if self._sharex is not None and other is not self._sharex: + raise ValueError("x-axis is already shared") + self._shared_x_axes.join(self, other) + self._sharex = other + self.xaxis.major = other.xaxis.major # Ticker instances holding + self.xaxis.minor = other.xaxis.minor # locator and formatter. + x0, x1 = other.get_xlim() + self.set_xlim(x0, x1, emit=False, auto=other.get_autoscalex_on()) + self.xaxis._scale = other.xaxis._scale + + def sharey(self, other): + """ + Share the y-axis with *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. + """ + cbook._check_isinstance(_AxesBase, other=other) + if self._sharey is not None and other is not self._sharey: + raise ValueError("y-axis is already shared") + self._shared_y_axes.join(self, other) + self._sharey = other + self.yaxis.major = other.yaxis.major # Ticker instances holding + self.yaxis.minor = other.yaxis.minor # locator and formatter. + y0, y1 = other.get_ylim() + self.set_ylim(y0, y1, emit=False, auto=other.get_autoscaley_on()) + self.yaxis._scale = other.yaxis._scale + def cla(self): """Clear the current axes.""" # Note: this is called by Axes.__init__() @@ -1031,38 +1066,25 @@ def cla(self): self.callbacks = cbook.CallbackRegistry() if self._sharex is not None: - # major and minor are axis.Ticker class instances with - # locator and formatter attributes - self.xaxis.major = self._sharex.xaxis.major - self.xaxis.minor = self._sharex.xaxis.minor - x0, x1 = self._sharex.get_xlim() - self.set_xlim(x0, x1, emit=False, - auto=self._sharex.get_autoscalex_on()) - self.xaxis._scale = self._sharex.xaxis._scale + self.sharex(self._sharex) else: self.xaxis._set_scale('linear') try: self.set_xlim(0, 1) except TypeError: pass - if self._sharey is not None: - self.yaxis.major = self._sharey.yaxis.major - self.yaxis.minor = self._sharey.yaxis.minor - y0, y1 = self._sharey.get_ylim() - self.set_ylim(y0, y1, emit=False, - auto=self._sharey.get_autoscaley_on()) - self.yaxis._scale = self._sharey.yaxis._scale + self.sharey(self._sharey) else: self.yaxis._set_scale('linear') try: self.set_ylim(0, 1) except TypeError: pass + # update the minor locator for x and y axis based on rcParams if mpl.rcParams['xtick.minor.visible']: self.xaxis.set_minor_locator(mticker.AutoMinorLocator()) - if mpl.rcParams['ytick.minor.visible']: self.yaxis.set_minor_locator(mticker.AutoMinorLocator()) @@ -1100,19 +1122,26 @@ def cla(self): size=mpl.rcParams['axes.titlesize'], weight=mpl.rcParams['axes.titleweight']) + y = mpl.rcParams['axes.titley'] + if y is None: + y = 1.0 + self._autotitlepos = True + else: + self._autotitlepos = False + self.title = mtext.Text( - x=0.5, y=1.0, text='', + x=0.5, y=y, text='', fontproperties=props, verticalalignment='baseline', horizontalalignment='center', ) self._left_title = mtext.Text( - x=0.0, y=1.0, text='', + x=0.0, y=y, text='', fontproperties=props.copy(), verticalalignment='baseline', horizontalalignment='left', ) self._right_title = mtext.Text( - x=1.0, y=1.0, text='', + x=1.0, y=y, text='', fontproperties=props.copy(), verticalalignment='baseline', horizontalalignment='right', @@ -1121,8 +1150,6 @@ def cla(self): # refactor this out so it can be called in ax.set_title if # pad argument used... self._set_title_offset_trans(title_offset_points) - # determine if the title position has been set manually: - self._autotitlepos = None for _title in (self.title, self._left_title, self._right_title): self._set_artist_props(_title) @@ -1144,11 +1171,10 @@ def cla(self): self._shared_x_axes.clean() self._shared_y_axes.clean() - if self._sharex: + if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) - - if self._sharey: + if self._sharey is not None: self.yaxis.set_visible(yaxis_visible) self.patch.set_visible(patch_visible) @@ -2601,16 +2627,6 @@ def _update_title_position(self, renderer): titles = (self.title, self._left_title, self._right_title) - if self._autotitlepos is None: - for title in titles: - x, y = title.get_position() - if not np.isclose(y, 1.0): - self._autotitlepos = False - _log.debug('not adjusting title pos because a title was ' - 'already placed manually: %f', y) - return - self._autotitlepos = True - for title in titles: x, _ = title.get_position() # need to start again in case of window resizing @@ -2742,9 +2758,10 @@ def draw(self, renderer=None, inframe=False): def draw_artist(self, a): """ - This method can only be used after an initial draw which - caches the renderer. It is used to efficiently update Axes - data (axis ticks, labels, etc are not updated). + Efficiently redraw a single artist. + + This method can only be used after an initial draw which caches the + renderer. """ if self.figure._cachedRenderer is None: raise AttributeError("draw_artist can only be used after an " @@ -2753,9 +2770,10 @@ def draw_artist(self, a): def redraw_in_frame(self): """ - This method can only be used after an initial draw which - caches the renderer. It is used to efficiently update Axes - data (axis ticks, labels, etc are not updated). + Efficiently redraw Axes data, but not axis ticks, labels, etc. + + This method can only be used after an initial draw which caches the + renderer. """ if self.figure._cachedRenderer is None: raise AttributeError("redraw_in_frame can only be used after an " @@ -3001,7 +3019,8 @@ def locator_params(self, axis='both', tight=None, **kwargs): scalex=update_x, scaley=update_y) def tick_params(self, axis='both', **kwargs): - """Change the appearance of ticks, tick labels, and gridlines. + """ + Change the appearance of ticks, tick labels, and gridlines. Tick properties that are not explicitly set using the keyword arguments remain unchanged unless *reset* is True. @@ -3828,18 +3847,15 @@ def _set_view_from_bbox(self, bbox, direction='in', twiny : bool Whether this axis is twinned in the *y*-direction. """ - Xmin, Xmax = self.get_xlim() - Ymin, Ymax = self.get_ylim() - if len(bbox) == 3: - # Zooming code - xp, yp, scl = bbox + Xmin, Xmax = self.get_xlim() + Ymin, Ymax = self.get_ylim() + + xp, yp, scl = bbox # Zooming code - # Should not happen - if scl == 0: + if scl == 0: # Should not happen scl = 1. - # direction = 'in' if scl > 1: direction = 'in' else: @@ -3868,90 +3884,51 @@ def _set_view_from_bbox(self, bbox, direction='in', "of length 3 or 4. Ignoring the view change.") return - # Just grab bounding box - lastx, lasty, x, y = bbox - - # zoom to rect - inverse = self.transData.inverted() - (lastx, lasty), (x, y) = inverse.transform([(lastx, lasty), (x, y)]) - - if twinx: - x0, x1 = Xmin, Xmax - else: - if Xmin < Xmax: - if x < lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 < Xmin: - x0 = Xmin - if x1 > Xmax: - x1 = Xmax - else: - if x > lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 > Xmin: - x0 = Xmin - if x1 < Xmax: - x1 = Xmax - - if twiny: - y0, y1 = Ymin, Ymax - else: - if Ymin < Ymax: - if y < lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 < Ymin: - y0 = Ymin - if y1 > Ymax: - y1 = Ymax - else: - if y > lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 > Ymin: - y0 = Ymin - if y1 < Ymax: - y1 = Ymax - - if direction == 'in': - if mode == 'x': - self.set_xlim((x0, x1)) - elif mode == 'y': - self.set_ylim((y0, y1)) - else: - self.set_xlim((x0, x1)) - self.set_ylim((y0, y1)) - elif direction == 'out': - if self.get_xscale() == 'log': - alpha = np.log(Xmax / Xmin) / np.log(x1 / x0) - rx1 = pow(Xmin / x0, alpha) * Xmin - rx2 = pow(Xmax / x0, alpha) * Xmin - else: - alpha = (Xmax - Xmin) / (x1 - x0) - rx1 = alpha * (Xmin - x0) + Xmin - rx2 = alpha * (Xmax - x0) + Xmin - if self.get_yscale() == 'log': - alpha = np.log(Ymax / Ymin) / np.log(y1 / y0) - ry1 = pow(Ymin / y0, alpha) * Ymin - ry2 = pow(Ymax / y0, alpha) * Ymin - else: - alpha = (Ymax - Ymin) / (y1 - y0) - ry1 = alpha * (Ymin - y0) + Ymin - ry2 = alpha * (Ymax - y0) + Ymin - - if mode == 'x': - self.set_xlim((rx1, rx2)) - elif mode == 'y': - self.set_ylim((ry1, ry2)) - else: - self.set_xlim((rx1, rx2)) - self.set_ylim((ry1, ry2)) + # Original limits. + xmin0, xmax0 = self.get_xbound() + ymin0, ymax0 = self.get_ybound() + # The zoom box in screen coords. + startx, starty, stopx, stopy = bbox + # Convert to data coords. + (startx, starty), (stopx, stopy) = self.transData.inverted().transform( + [(startx, starty), (stopx, stopy)]) + # Clip to axes limits. + xmin, xmax = np.clip(sorted([startx, stopx]), xmin0, xmax0) + ymin, ymax = np.clip(sorted([starty, stopy]), ymin0, ymax0) + # Don't double-zoom twinned axes or if zooming only the other axis. + if twinx or mode == "y": + xmin, xmax = xmin0, xmax0 + if twiny or mode == "x": + ymin, ymax = ymin0, ymax0 + + if direction == "in": + new_xbound = xmin, xmax + new_ybound = ymin, ymax + + elif direction == "out": + x_trf = self.xaxis.get_transform() + sxmin0, sxmax0, sxmin, sxmax = x_trf.transform( + [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). + sxmin1 = sxmin0 - factor * (sxmin - sxmin0) + sxmax1 = sxmax0 + factor * (sxmax0 - sxmax) + # And back to data space. + new_xbound = x_trf.inverted().transform([sxmin1, sxmax1]) + + y_trf = self.yaxis.get_transform() + symin0, symax0, symin, symax = y_trf.transform( + [ymin0, ymax0, ymin, ymax]) + factor = (symax0 - symin0) / (symax - symin) + symin1 = symin0 - factor * (symin - symin0) + symax1 = symax0 + factor * (symax0 - symax) + new_ybound = y_trf.inverted().transform([symin1, symax1]) + + if not twinx and mode != "y": + self.set_xbound(new_xbound) + if not twiny and mode != "x": + self.set_ybound(new_ybound) def start_pan(self, x, y, button): """ diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 90511e58b394..bba7d871e1fc 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -179,7 +179,7 @@ def _make_twin_axes(self, *args, **kwargs): @functools.lru_cache(None) def subplot_class_factory(axes_class=None): """ - This makes a new class that inherits from `.SubplotBase` and the + Make a new class that inherits from `.SubplotBase` and the given axes_class (which is assumed to be a subclass of `.axes.Axes`). This is perhaps a little bit roundabout to make a new class on the fly like this, but it means that a new Subplot class does @@ -207,8 +207,9 @@ def subplot_class_factory(axes_class=None): def _picklable_subplot_class_constructor(axes_class): """ - This stub class exists to return the appropriate subplot class when called - with an axes class. This is purely to allow pickling of Axes and Subplots. + Stub factory that returns an empty instance of the appropriate subplot + class when called with an axes class. This is purely to allow pickling of + Axes and Subplots. """ subplot_class = subplot_class_factory(axes_class) return subplot_class.__new__(subplot_class) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 4b474ca2a54d..03a4e7ea0f92 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1172,14 +1172,14 @@ def get_majorticklabels(self): ticks = self.get_major_ticks() labels1 = [tick.label1 for tick in ticks if tick.label1.get_visible()] labels2 = [tick.label2 for tick in ticks if tick.label2.get_visible()] - return cbook.silent_list('Text major ticklabel', labels1 + labels2) + return labels1 + labels2 def get_minorticklabels(self): """Return this Axis' minor tick labels, as a list of `~.text.Text`.""" ticks = self.get_minor_ticks() labels1 = [tick.label1 for tick in ticks if tick.label1.get_visible()] labels2 = [tick.label2 for tick in ticks if tick.label2.get_visible()] - return cbook.silent_list('Text minor ticklabel', labels1 + labels2) + return labels1 + labels2 def get_ticklabels(self, minor=False, which=None): """ @@ -1885,8 +1885,7 @@ def __init__(self, *args, **kwargs): self.offset_text_position = 'bottom' def contains(self, mouseevent): - """Test whether the mouse event occurred in the x axis. - """ + """Test whether the mouse event occurred in the x axis.""" inside, info = self._default_contains(mouseevent) if inside is not None: return inside, info diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 64f1e0eb8c11..9338908cda3f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -29,7 +29,7 @@ """ from contextlib import contextmanager -from enum import IntEnum +from enum import Enum, IntEnum import functools import importlib import io @@ -41,10 +41,12 @@ import numpy as np +import matplotlib as mpl from matplotlib import ( backend_tools as tools, cbook, colors, textpath, tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_managers import ToolManager from matplotlib.transforms import Affine2D from matplotlib.path import Path @@ -116,7 +118,8 @@ def get_registered_canvas_class(format): class RendererBase: - """An abstract base class to handle drawing/rendering operations. + """ + An abstract base class to handle drawing/rendering operations. The following methods must be implemented in the backend for full functionality (though just implementing :meth:`draw_path` alone would @@ -235,9 +238,8 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, edgecolors): """ - This provides a fallback implementation of - :meth:`draw_quad_mesh` that generates paths and then calls - :meth:`draw_path_collection`. + Fallback implementation of :meth:`draw_quad_mesh` that generates paths + and then calls :meth:`draw_path_collection`. """ from matplotlib.collections import QuadMesh @@ -296,9 +298,8 @@ def draw_gouraud_triangles(self, gc, triangles_array, colors_array, def _iter_collection_raw_paths(self, master_transform, paths, all_transforms): """ - This is a helper method (along with :meth:`_iter_collection`) to make - it easier to write a space-efficient :meth:`draw_path_collection` - implementation in a backend. + Helper method (along with :meth:`_iter_collection`) to implement + :meth:`draw_path_collection` in a space-efficient manner. This method yields all of the base path/transform combinations, given a master transform, a list of paths and @@ -346,10 +347,8 @@ def _iter_collection(self, gc, master_transform, all_transforms, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): """ - This is a helper method (along with - :meth:`_iter_collection_raw_paths`) to make it easier to write - a space-efficient :meth:`draw_path_collection` implementation in a - backend. + Helper method (along with :meth:`_iter_collection_raw_paths`) to + implement :meth:`draw_path_collection` in a space-efficient manner. This method yields all of the path, offset and graphics context combinations to draw the path collection. The caller @@ -2306,7 +2305,7 @@ def stop_event_loop(self): self._looping = False -def key_press_handler(event, canvas, toolbar=None): +def key_press_handler(event, canvas=None, toolbar=None): """ Implement the default Matplotlib key bindings for the canvas and toolbar described at :ref:`key-event-handling`. @@ -2314,16 +2313,24 @@ def key_press_handler(event, canvas, toolbar=None): Parameters ---------- event : `KeyEvent` - a key press/release event - canvas : `FigureCanvasBase` - the backend-specific canvas instance - toolbar : `NavigationToolbar2` - the navigation cursor toolbar + A key press/release event. + canvas : `FigureCanvasBase`, default: ``event.canvas`` + The backend-specific canvas instance. This parameter is kept for + back-compatibility, but, if set, should always be equal to + ``event.canvas``. + toolbar : `NavigationToolbar2`, default: ``event.canvas.toolbar`` + The navigation cursor toolbar. This parameter is kept for + back-compatibility, but, if set, should always be equal to + ``event.canvas.toolbar``. """ # these bindings happen whether you are over an axes or not if event.key is None: return + if canvas is None: + canvas = event.canvas + if toolbar is None: + toolbar = canvas.toolbar # Load key-mappings from rcParams. fullscreen_keys = rcParams['keymap.fullscreen'] @@ -2483,10 +2490,17 @@ def _get_uniform_gridstate(ticks): a.set_navigate(i == n) -def button_press_handler(event, canvas, toolbar=None): +def button_press_handler(event, canvas=None, toolbar=None): """ The default Matplotlib button actions for extra mouse buttons. + + Parameters are as for `key_press_handler`, except that *event* is a + `MouseEvent`. """ + if canvas is None: + canvas = event.canvas + if toolbar is None: + toolbar = canvas.toolbar if toolbar is not None: button_name = str(MouseButton(event.button)) if button_name in rcParams['keymap.back']: @@ -2573,7 +2587,9 @@ def __init__(self, canvas, num): 'button_press_event', self.button_press) - self.toolmanager = None + self.toolmanager = (ToolManager(canvas.figure) + if mpl.rcParams['toolbar'] == 'toolmanager' + else None) self.toolbar = None @self.canvas.figure.add_axobserver @@ -2610,12 +2626,12 @@ def key_press(self, event): :ref:`key-event-handling`. """ if rcParams['toolbar'] != 'toolmanager': - key_press_handler(event, self.canvas, self.canvas.toolbar) + key_press_handler(event) def button_press(self, event): """The default Matplotlib button actions for extra mouse buttons.""" if rcParams['toolbar'] != 'toolmanager': - button_press_handler(event, self.canvas, self.canvas.toolbar) + button_press_handler(event) def get_window_title(self): """ @@ -2635,6 +2651,15 @@ def set_window_title(self, title): cursors = tools.cursors +class _Mode(str, Enum): + NONE = "" + PAN = "pan/zoom" + ZOOM = "zoom rect" + + def __str__(self): + return self.value + + class NavigationToolbar2: """ Base class for the navigation cursor, version 2 @@ -2699,19 +2724,20 @@ def __init__(self, canvas): canvas.toolbar = self self._nav_stack = cbook.Stack() self._xypress = None # location and axis info at the time of the press - self._idPress = None - self._idRelease = None - self._active = None # This cursor will be set after the initial draw. self._lastCursor = cursors.POINTER self._init_toolbar() + self._id_press = self.canvas.mpl_connect( + 'button_press_event', self._zoom_pan_handler) + self._id_release = self.canvas.mpl_connect( + 'button_release_event', self._zoom_pan_handler) self._id_drag = self.canvas.mpl_connect( 'motion_notify_event', self.mouse_move) - self._id_zoom = None + self._zoom_info = None self._button_pressed = None # determined by button pressed at start - self.mode = '' # a mode string for the status bar + self.mode = _Mode.NONE # a mode string for the status bar self.set_history_buttons() def set_message(self, s): @@ -2789,17 +2815,17 @@ def _update_cursor(self, event): """ Update the cursor after a mouse move event or a tool (de)activation. """ - if not event.inaxes or not self._active: + if not event.inaxes or not self.mode: if self._lastCursor != cursors.POINTER: self.set_cursor(cursors.POINTER) self._lastCursor = cursors.POINTER else: - if (self._active == 'ZOOM' + if (self.mode == _Mode.ZOOM and self._lastCursor != cursors.SELECT_REGION): self.set_cursor(cursors.SELECT_REGION) self._lastCursor = cursors.SELECT_REGION - elif (self._active == 'PAN' and - self._lastCursor != cursors.MOVE): + elif (self.mode == _Mode.PAN + and self._lastCursor != cursors.MOVE): self.set_cursor(cursors.MOVE) self._lastCursor = cursors.MOVE @@ -2854,40 +2880,32 @@ def mouse_move(self, event): else: self.set_message(self.mode) + def _zoom_pan_handler(self, event): + if self.mode == _Mode.PAN: + if event.name == "button_press_event": + self.press_pan(event) + elif event.name == "button_release_event": + self.release_pan(event) + if self.mode == _Mode.ZOOM: + if event.name == "button_press_event": + self.press_zoom(event) + elif event.name == "button_release_event": + self.release_zoom(event) + def pan(self, *args): """ - Activate the pan/zoom tool. + Toggle the pan/zoom tool. Pan with left button, zoom with right. """ - # set the pointer icon and button press funcs to the - # appropriate callbacks - - if self._active == 'PAN': - self._active = None + if self.mode == _Mode.PAN: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) else: - self._active = 'PAN' - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect( - 'button_press_event', self.press_pan) - self._idRelease = self.canvas.mpl_connect( - 'button_release_event', self.release_pan) - self.mode = 'pan/zoom' + self.mode = _Mode.PAN self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - + a.set_navigate_mode(self.mode) self.set_message(self.mode) def press(self, event): @@ -2917,37 +2935,24 @@ def press_pan(self, event): def press_zoom(self, event): """Callback for mouse button press in zoom to rect mode.""" - # If we're already in the middle of a zoom, pressing another - # button works to "cancel" - if self._id_zoom is not None: - self.canvas.mpl_disconnect(self._id_zoom) - self.release(event) - self.draw() - self._xypress = None - self._button_pressed = None - self._id_zoom = None + if event.button not in [1, 3]: return - - if event.button in [1, 3]: - self._button_pressed = event.button - else: - self._button_pressed = None + if event.x is None or event.y is None: + return + axes = [a for a in self.canvas.figure.get_axes() + if a.in_axes(event) and a.get_navigate() and a.can_zoom()] + if not axes: return - if self._nav_stack() is None: - # set the home button to this view - self.push_current() - - x, y = event.x, event.y - self._xypress = [] - for a in self.canvas.figure.get_axes(): - if (x is not None and y is not None and a.in_axes(event) and - a.get_navigate() and a.can_zoom()): - self._xypress.append((x, y, a)) - - self._id_zoom = self.canvas.mpl_connect( - 'motion_notify_event', self.drag_zoom) - + self.push_current() # set the home button to this view + id_zoom = self.canvas.mpl_connect( + "motion_notify_event", self.drag_zoom) + self._zoom_info = { + "direction": "in" if event.button == 1 else "out", + "start_xy": (event.x, event.y), + "axes": axes, + "cid": id_zoom, + } self.press(event) def push_current(self): @@ -2980,7 +2985,7 @@ def release_pan(self, event): self._button_pressed = None self.push_current() self.release(event) - self.draw() + self._draw() def drag_pan(self, event): """Callback for dragging in pan/zoom mode.""" @@ -2992,71 +2997,65 @@ def drag_pan(self, event): def drag_zoom(self, event): """Callback for dragging in zoom mode.""" - if self._xypress: - x, y = event.x, event.y - lastx, lasty, a = self._xypress[0] - (x1, y1), (x2, y2) = np.clip( - [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max) - if event.key == "x": - y1, y2 = a.bbox.intervaly - elif event.key == "y": - x1, x2 = a.bbox.intervalx - self.draw_rubberband(event, x1, y1, x2, y2) + start_xy = self._zoom_info["start_xy"] + ax = self._zoom_info["axes"][0] + (x1, y1), (x2, y2) = np.clip( + [start_xy, [event.x, event.y]], ax.bbox.min, ax.bbox.max) + if event.key == "x": + y1, y2 = ax.bbox.intervaly + elif event.key == "y": + x1, x2 = ax.bbox.intervalx + self.draw_rubberband(event, x1, y1, x2, y2) def release_zoom(self, event): """Callback for mouse button release in zoom to rect mode.""" - if self._id_zoom is not None: - self.canvas.mpl_disconnect(self._id_zoom) - self._id_zoom = None + if self._zoom_info is None: + return + # We don't check the event button here, so that zooms can be cancelled + # by (pressing and) releasing another mouse button. + self.canvas.mpl_disconnect(self._zoom_info["cid"]) self.remove_rubberband() - if not self._xypress: - return - - last_a = [] + start_x, start_y = self._zoom_info["start_xy"] - for lastx, lasty, a in self._xypress: + for i, ax in enumerate(self._zoom_info["axes"]): x, y = event.x, event.y # ignore singular clicks - 5 pixels is a threshold # allows the user to "cancel" a zoom action # by zooming by less than 5 pixels - if ((abs(x - lastx) < 5 and event.key != "y") or - (abs(y - lasty) < 5 and event.key != "x")): + if ((abs(x - start_x) < 5 and event.key != "y") or + (abs(y - start_y) < 5 and event.key != "x")): self._xypress = None self.release(event) - self.draw() + self._draw() return - # detect twinx, twiny axes and avoid double zooming - twinx, twiny = False, False - if last_a: - for la in last_a: - if a.get_shared_x_axes().joined(a, la): - twinx = True - if a.get_shared_y_axes().joined(a, la): - twiny = True - last_a.append(a) - - if self._button_pressed == 1: - direction = 'in' - elif self._button_pressed == 3: - direction = 'out' - else: - continue + # Detect whether this axes is twinned with an earlier axes in the + # list of zoomed axes, to avoid double zooming. + twinx = any(ax.get_shared_x_axes().joined(ax, prev) + for prev in self._zoom_info["axes"][:i]) + twiny = any(ax.get_shared_y_axes().joined(ax, prev) + for prev in self._zoom_info["axes"][:i]) - a._set_view_from_bbox((lastx, lasty, x, y), direction, - event.key, twinx, twiny) + ax._set_view_from_bbox( + (start_x, start_y, x, y), self._zoom_info["direction"], + event.key, twinx, twiny) - self.draw() - self._xypress = None - self._button_pressed = None + self._draw() + self._zoom_info = None self.push_current() self.release(event) + @cbook.deprecated("3.3", alternative="toolbar.canvas.draw_idle()") def draw(self): """Redraw the canvases, update the locators.""" + self._draw() + + # Can be removed once Locator.refresh() is removed, and replaced by an + # inline call to self.canvas.draw_idle(). + def _draw(self): for a in self.canvas.figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) @@ -3069,7 +3068,7 @@ def draw(self): locators.append(yaxis.get_minor_locator()) for loc in locators: - loc.refresh() + mpl.ticker._if_refresh_overridden_call_and_emit_deprec(loc) self.canvas.draw_idle() def _update_view(self): @@ -3110,33 +3109,15 @@ def update(self): self.set_history_buttons() def zoom(self, *args): - """Activate zoom to rect mode.""" - if self._active == 'ZOOM': - self._active = None + """Toggle zoom to rect mode.""" + if self.mode == _Mode.ZOOM: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) else: - self._active = 'ZOOM' - - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect('button_press_event', - self.press_zoom) - self._idRelease = self.canvas.mpl_connect('button_release_event', - self.release_zoom) - self.mode = 'zoom rect' + self.mode = _Mode.ZOOM self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - + a.set_navigate_mode(self.mode) self.set_message(self.mode) def set_history_buttons(self): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 302b63a861d0..b9e4674c1f64 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -636,8 +636,14 @@ def update_home_views(self, figure=None): if a not in self.home_views[figure]: self.home_views[figure][a] = a._get_view() + @cbook.deprecated("3.3", alternative="self.figure.canvas.draw_idle()") def refresh_locators(self): """Redraw the canvases, update the locators.""" + self._refresh_locators() + + # Can be removed once Locator.refresh() is removed, and replaced by an + # inline call to self.figure.canvas.draw_idle(). + def _refresh_locators(self): for a in self.figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) @@ -654,7 +660,7 @@ def refresh_locators(self): locators.append(zaxis.get_minor_locator()) for loc in locators: - loc.refresh() + mpl.ticker._if_refresh_overridden_call_and_emit_deprec(loc) self.figure.canvas.draw_idle() def home(self): @@ -808,7 +814,7 @@ def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.toolmanager.trigger_tool('rubberband', self) - self.toolmanager.get_tool(_views_positions).refresh_locators() + self.toolmanager.get_tool(_views_positions)._refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -935,7 +941,7 @@ def _cancel_action(self): self._xypress = [] self.figure.canvas.mpl_disconnect(self._id_drag) self.toolmanager.messagelock.release(self) - self.toolmanager.get_tool(_views_positions).refresh_locators() + self.toolmanager.get_tool(_views_positions)._refresh_locators() def _press(self, event): if event.button == 1: @@ -986,7 +992,7 @@ def _mouse_move(self, event): class ToolHelpBase(ToolBase): description = 'Print tool list, shortcuts and description' default_keymap = mpl.rcParams['keymap.help'] - image = 'help.png' + image = 'help' @staticmethod def format_shortcut(key_sequence): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 600722dfcc8d..bbe94cd48195 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -15,7 +15,6 @@ from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, StatusbarBase, TimerBase, ToolContainerBase, cursors) -from matplotlib.backend_managers import ToolManager from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool @@ -260,7 +259,9 @@ def idle_draw(*args): self._idle_callback = self._tkcanvas.after_idle(idle_draw) def get_tk_widget(self): - """Return the Tk widget used to implement FigureCanvasTkAgg. + """ + Return the Tk widget used to implement FigureCanvasTkAgg. + Although the initial implementation uses a Tk canvas, this routine is intended to hide that fact. """ @@ -403,17 +404,13 @@ class FigureManagerTk(FigureManagerBase): The tk.Toolbar window : tk.Window The tk.Window - """ + def __init__(self, canvas, num, window): FigureManagerBase.__init__(self, canvas, num) self.window = window self.window.withdraw() self.set_window_title("Figure %d" % num) - self.canvas = canvas - # If using toolmanager it has to be present when initializing the - # toolbar - self.toolmanager = self._get_toolmanager() # packing toolbar first, because if space is getting low, last packed # widget is getting shrunk first (-> the canvas) self.toolbar = self._get_toolbar() @@ -438,13 +435,6 @@ def _get_toolbar(self): toolbar = None return toolbar - def _get_toolmanager(self): - if mpl.rcParams['toolbar'] == 'toolmanager': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - def resize(self, width, height): self.canvas._tkcanvas.configure(width=width, height=height) @@ -500,7 +490,6 @@ class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame): ``pack_toolbar=False``. """ def __init__(self, canvas, window, *, pack_toolbar=True): - self.canvas = canvas # Avoid using self.window (prefer self.canvas.get_tk_widget().master), # so that Tool implementations can reuse the methods. self.window = window @@ -622,11 +611,6 @@ def save_figure(self, *args): except Exception as e: tkinter.messagebox.showerror("Error saving file", str(e)) - def update(self): - self._axes = self.canvas.figure.axes - with _restore_foreground_window_at_end(): - NavigationToolbar2.update(self) - class ToolTip: """ diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 60537cf6443a..7fdc6070b9d4 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -10,7 +10,6 @@ from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, StatusbarBase, TimerBase, ToolContainerBase, cursors) -from matplotlib.backend_managers import ToolManager from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool @@ -343,7 +342,6 @@ def __init__(self, canvas, num): w = int(self.canvas.figure.bbox.width) h = int(self.canvas.figure.bbox.height) - self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar() self.statusbar = None @@ -420,14 +418,6 @@ def _get_toolbar(self): toolbar = None return toolbar - def _get_toolmanager(self): - # must be initialised after toolbar has been set - if mpl.rcParams['toolbar'] == 'toolmanager': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - def get_window_title(self): return self.window.get_title() @@ -528,7 +518,7 @@ def _update_buttons_checked(self): button = self._gtk_ids.get(name) if button: with button.handler_block(button._signal_handler): - button.set_active(self._active == active) + button.set_active(self.mode.name == active) def pan(self, *args): super().pan(*args) diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index cc605f05bfca..9cc36cb6144d 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -24,8 +24,7 @@ def _render_figure(self, width, height): backend_agg.FigureCanvasAgg.draw(self) def on_draw_event(self, widget, ctx): - """GtkDrawable draw event, like expose_event in GTK 2.X. - """ + """GtkDrawable draw event, like expose_event in GTK 2.X.""" allocation = self.get_allocation() w, h = allocation.width, allocation.height diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 32645ff92a5f..8056cf549551 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -226,7 +226,9 @@ def pdfRepr(obj): class Reference: - """PDF reference object. + """ + PDF reference object. + Use PdfFile.reserveObject() to create References. """ @@ -1223,7 +1225,8 @@ def alphaState(self, alpha): return name def _soft_mask_state(self, smask): - """Return an ExtGState that sets the soft mask to the given shading. + """ + Return an ExtGState that sets the soft mask to the given shading. Parameters ---------- @@ -1345,7 +1348,8 @@ def writeHatches(self): self.writeObject(self.hatchObject, hatchDict) def addGouraudTriangles(self, points, colors): - """Add a Gouraud triangle shading + """ + Add a Gouraud triangle shading. Parameters ---------- @@ -1603,11 +1607,12 @@ def writePath(self, path, transform, clip=False, sketch=None): self.output(*cmds) def reserveObject(self, name=''): - """Reserve an ID for an indirect object. + """ + Reserve an ID for an indirect object. + The name is used for debugging in case we forget to print out the object with writeObject. """ - id = next(self._object_seq) self.xrefTable.append([None, 0, name]) return Reference(id) @@ -1720,7 +1725,7 @@ def check_gc(self, gc, fillcolor=None): @cbook.deprecated("3.3") def track_characters(self, *args, **kwargs): - """Keeps track of which characters are required from each font.""" + """Keep track of which characters are required from each font.""" self.file._character_tracker.track(*args, **kwargs) @cbook.deprecated("3.3") @@ -2455,7 +2460,7 @@ def infodict(self): def savefig(self, figure=None, **kwargs): """ - Saves a `.Figure` to this file as a new page. + Save a `.Figure` to this file as a new page. Any other keyword arguments are passed to `~.Figure.savefig`. diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index e47cfe601ab0..67a2b580469e 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -395,7 +395,7 @@ class RendererPgf(RendererBase): @cbook._delete_parameter("3.3", "dummy") def __init__(self, figure, fh, dummy=False): """ - Creates a new PGF renderer that translates any drawing instruction + Create a new PGF renderer that translates any drawing instruction into text commands to be interpreted in a latex pgfpicture environment. Attributes @@ -1004,7 +1004,7 @@ def __init__(self, filename, *, keep_empty=True, metadata=None): filename : str or path-like Plots using `PdfPages.savefig` will be written to a file at this location. Any older file with the same name is overwritten. - keep_empty : bool, optional + keep_empty : bool, default: True If set to False, then empty pdf files will be deleted automatically when closed. metadata : dict, optional @@ -1111,7 +1111,7 @@ def _run_latex(self): def savefig(self, figure=None, **kwargs): """ - Saves a `.Figure` to this file as a new page. + Save a `.Figure` to this file as a new page. Any other keyword arguments are passed to `~.Figure.savefig`. diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index d95d6ea60f2b..03693c859440 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -176,7 +176,7 @@ def used_characters(self): @cbook.deprecated("3.3") def track_characters(self, *args, **kwargs): - """Keeps track of which characters are required from each font.""" + """Keep track of which characters are required from each font.""" self._character_tracker.track(*args, **kwargs) @cbook.deprecated("3.3") @@ -484,8 +484,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): r'\psfrag{%s}[Bl][Bl][1][%f]{\fontsize{%f}{%f}%s}' % ( thetext, angle, fontsize, fontsize*1.25, tex)) else: - # Stick to the bottom alignment, but this may give incorrect - # baseline some times. + # Stick to the bottom alignment. pos = _nums_to_str(x-corr, y-bl) self.psfrag.append( r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % ( @@ -1098,18 +1097,20 @@ def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, with mpl.rc_context({ "text.latex.preamble": mpl.rcParams["text.latex.preamble"] + - r"\usepackage{psfrag,color}" - r"\usepackage[dvips]{graphicx}" - r"\PassOptionsToPackage{dvips}{geometry}"}): + r"\usepackage{psfrag,color}""\n" + r"\usepackage[dvips]{graphicx}""\n" + r"\geometry{papersize={%(width)sin,%(height)sin}," + r"body={%(width)sin,%(height)sin},margin=0in}" + % {"width": paper_width, "height": paper_height} + }): dvifile = TexManager().make_dvi( - r"\newgeometry{papersize={%(width)sin,%(height)sin}," - r"body={%(width)sin,%(height)sin}, margin={0in,0in}}""\n" - r"\begin{figure}" - r"\centering\leavevmode%(psfrags)s" - r"\includegraphics*[angle=%(angle)s]{%(epsfile)s}" + "\n" + r"\begin{figure}""\n" + r" \centering\leavevmode""\n" + r" %(psfrags)s""\n" + r" \includegraphics*[angle=%(angle)s]{%(epsfile)s}""\n" r"\end{figure}" % { - "width": paper_width, "height": paper_height, "psfrags": "\n".join(psfrags), "angle": 90 if orientation == 'landscape' else 0, "epsfile": pathlib.Path(tmpfile).resolve().as_posix(), diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 6a05c90a4dee..1bb4f55e5397 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -15,11 +15,9 @@ TimerBase, cursors, ToolContainerBase, StatusbarBase, MouseButton) import matplotlib.backends.qt_editor.figureoptions as figureoptions from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool -from matplotlib.backend_managers import ToolManager - +from . import qt_compat from .qt_compat import ( - QtCore, QtGui, QtWidgets, _isdeleted, _getSaveFileName, - is_pyqt5, __version__, QT_API) + QtCore, QtGui, QtWidgets, _isdeleted, is_pyqt5, __version__, QT_API) backend_version = __version__ @@ -255,12 +253,7 @@ def _update_figure_dpi(self): @property def _dpi_ratio(self): - # Not available on Qt4 or some older Qt5. - try: - # self.devicePixelRatio() returns 0 in rare cases - return self.devicePixelRatio() or 1 - except AttributeError: - return 1 + return qt_compat._devicePixelRatio(self) def _update_dpi(self): # As described in __init__ above, we need to be careful in cases with @@ -299,14 +292,14 @@ def leaveEvent(self, event): FigureCanvasBase.leave_notify_event(self, guiEvent=event) def mouseEventCoords(self, pos): - """Calculate mouse coordinates in physical pixels + """ + Calculate mouse coordinates in physical pixels. Qt5 use logical pixels, but the figure is scaled to physical - pixels for rendering. Transform to physical pixels so that + pixels for rendering. Transform to physical pixels so that all of the down-stream transforms work as expected. Also, the origin is different and needs to be corrected. - """ dpi_ratio = self._dpi_ratio x = pos.x() @@ -448,8 +441,7 @@ def stop_event_loop(self, event=None): self._event_loop.quit() def draw(self): - """Render the figure, and queue a request for a Qt draw. - """ + """Render the figure, and queue a request for a Qt draw.""" # The renderer draw is done here; delaying causes problems with code # that uses the result of the draw() to update plot elements. if self._is_drawing: @@ -459,9 +451,8 @@ def draw(self): self.update() def draw_idle(self): - """Queue redraw of the Agg buffer and request Qt paintEvent. - """ - # The Agg draw needs to be handled by the same thread matplotlib + """Queue redraw of the Agg buffer and request Qt paintEvent.""" + # The Agg draw needs to be handled by the same thread Matplotlib # modifies the scene graph from. Post Agg draw request to the # current event loop in order to ensure thread affinity and to # accumulate multiple draw requests from event handling. @@ -520,12 +511,10 @@ class FigureManagerQT(FigureManagerBase): The qt.QToolBar window : qt.QMainWindow The qt.QMainWindow - """ def __init__(self, canvas, num): FigureManagerBase.__init__(self, canvas, num) - self.canvas = canvas self.window = MainWindow() self.window.closing.connect(canvas.close_event) self.window.closing.connect(self._widgetclosed) @@ -534,19 +523,15 @@ def __init__(self, canvas, num): image = str(cbook._get_data_path('images/matplotlib.svg')) self.window.setWindowIcon(QtGui.QIcon(image)) - # Give the keyboard focus to the figure instead of the - # manager; StrongFocus accepts both tab and click to focus and - # will enable the canvas to process event w/o clicking. - # ClickFocus only takes the focus is the window has been - # clicked - # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or - # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum + # Give the keyboard focus to the figure instead of the manager: + # StrongFocus accepts both tab and click to focus and will enable the + # canvas to process event without clicking. + # https://doc.qt.io/qt-5/qt.html#FocusPolicy-enum self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) self.canvas.setFocus() self.window._destroying = False - self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar(self.canvas, self.window) self.statusbar = None @@ -611,13 +596,6 @@ def _get_toolbar(self, canvas, parent): toolbar = None return toolbar - def _get_toolmanager(self): - if matplotlib.rcParams['toolbar'] == 'toolmanager': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - def resize(self, width, height): # these are Qt methods so they return sizes in 'virtual' pixels # so we do not need to worry about dpi scaling here. @@ -662,12 +640,9 @@ class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): def __init__(self, canvas, parent, coordinates=True): """coordinates: should we show the coordinates on the right?""" - self.canvas = canvas self._parent = parent self.coordinates = coordinates - self._actions = {} - """A mapping of toolitem method names to their QActions""" - + self._actions = {} # mapping of toolitem method names to QActions. QtWidgets.QToolBar.__init__(self, parent) NavigationToolbar2.__init__(self, canvas) @@ -676,12 +651,17 @@ def __init__(self, canvas, parent, coordinates=True): def parent(self): return self._parent + @cbook.deprecated( + "3.3", alternative="os.path.join(mpl.get_data_path(), 'images')") + @property + def basedir(self): + return str(cbook._get_data_path('images')) + def _icon(self, name, color=None): if is_pyqt5(): name = name.replace('.png', '_large.png') - pm = QtGui.QPixmap(os.path.join(self.basedir, name)) - if hasattr(pm, 'setDevicePixelRatio'): - pm.setDevicePixelRatio(self.canvas._dpi_ratio) + pm = QtGui.QPixmap(str(cbook._get_data_path('images', name))) + qt_compat._setDevicePixelRatio(pm, self.canvas._dpi_ratio) if color is not None: mask = pm.createMaskFromColor(QtGui.QColor('black'), QtCore.Qt.MaskOutColor) @@ -690,8 +670,6 @@ def _icon(self, name, color=None): return QtGui.QIcon(pm) def _init_toolbar(self): - self.basedir = str(cbook._get_data_path('images')) - background_color = self.palette().color(self.backgroundRole()) foreground_color = self.palette().color(self.foregroundRole()) icon_color = (foreground_color @@ -753,9 +731,9 @@ def edit_parameters(self): def _update_buttons_checked(self): # sync button checkstates to match active mode if 'pan' in self._actions: - self._actions['pan'].setChecked(self._active == 'PAN') + self._actions['pan'].setChecked(self.mode.name == 'PAN') if 'zoom' in self._actions: - self._actions['zoom'].setChecked(self._active == 'ZOOM') + self._actions['zoom'].setChecked(self.mode.name == 'ZOOM') def pan(self, *args): super().pan(*args) @@ -807,9 +785,9 @@ def save_figure(self, *args): filters.append(filter) filters = ';;'.join(filters) - fname, filter = _getSaveFileName(self.canvas.parent(), - "Choose a filename to save to", - start, filters, selectedFilter) + fname, filter = qt_compat._getSaveFileName( + self.canvas.parent(), "Choose a filename to save to", start, + filters, selectedFilter) if fname: # Save dir for next time, unless empty str (i.e., use cwd). if startpath != "": @@ -945,8 +923,7 @@ def _add_to_group(self, group, name, button, position): def _icon(self, name): pm = QtGui.QPixmap(name) - if hasattr(pm, 'setDevicePixelRatio'): - pm.setDevicePixelRatio(self.toolmanager.canvas._dpi_ratio) + qt_compat._setDevicePixelRatio(pm, self.toolmanager.canvas._dpi_ratio) return QtGui.QIcon(pm) def toggle_toolitem(self, name, toggled): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index feac9f31817c..f0b1955ec08d 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -129,7 +129,7 @@ def __flush(self, indent=True): def start(self, tag, attrib={}, **extra): """ - Opens a new element. Attributes can be given as keyword + Open a new element. Attributes can be given as keyword arguments, or as a string/string dictionary. The method returns an opaque identifier that can be passed to the :meth:`close` method, to close all open elements up to and including this one. @@ -162,7 +162,7 @@ def start(self, tag, attrib={}, **extra): def comment(self, comment): """ - Adds a comment to the output stream. + Add a comment to the output stream. Parameters ---------- @@ -175,7 +175,7 @@ def comment(self, comment): def data(self, text): """ - Adds character data to the output stream. + Add character data to the output stream. Parameters ---------- @@ -186,7 +186,7 @@ def data(self, text): def end(self, tag=None, indent=True): """ - Closes the current element (opened by the most recent call to + Close the current element (opened by the most recent call to :meth:`start`). Parameters @@ -214,7 +214,7 @@ def end(self, tag=None, indent=True): def close(self, id): """ - Closes open elements, up to (and including) the element identified + Close open elements, up to (and including) the element identified by the given identifier. Parameters @@ -227,7 +227,7 @@ def close(self, id): def element(self, tag, text=None, attrib={}, **extra): """ - Adds an entire element. This is the same as calling :meth:`start`, + Add an entire element. This is the same as calling :meth:`start`, :meth:`data`, and :meth:`end` in sequence. The *text* argument can be omitted. """ @@ -237,7 +237,7 @@ def element(self, tag, text=None, attrib={}, **extra): self.end(indent=False) def flush(self): - """Flushes the output stream.""" + """Flush the output stream.""" pass # replaced by the constructor diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index 19c8c822274b..bd5cab86deb3 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -1,5 +1,5 @@ """ -This is a fully functional do nothing backend to provide a template to backend +A fully functional, do-nothing backend intended as a template for backend writers. It is fully functional in that you can select it as a backend e.g. with :: diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 63c02979a6e8..0f84f23ca372 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -935,7 +935,8 @@ def __init__(self, num, fig): # By adding toolbar in sizer, we are able to put it at the bottom # of the frame - so appearance is closer to GTK version - self.toolmanager = self._get_toolmanager() + self.figmgr = FigureManagerWx(self.canvas, num, self) + statusbar = (StatusbarWx(self, self.toolmanager) if self.toolmanager else StatusBarWx(self)) self.SetStatusBar(statusbar) @@ -961,8 +962,6 @@ def __init__(self, num, fig): self.canvas.SetMinSize((2, 2)) - self.figmgr = FigureManagerWx(self.canvas, num, self) - self.Bind(wx.EVT_CLOSE, self._onClose) @cbook.deprecated("3.2", alternative="self.GetStatusBar()") @@ -970,6 +969,10 @@ def __init__(self, num, fig): def statusbar(self): return self.GetStatusBar() + @property + def toolmanager(self): + return self.figmgr.toolmanager + def _get_toolbar(self): if mpl.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2Wx(self.canvas) @@ -979,13 +982,6 @@ def _get_toolbar(self): toolbar = None return toolbar - def _get_toolmanager(self): - if mpl.rcParams['toolbar'] == 'toolmanager': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - def get_canvas(self, fig): return FigureCanvasWx(self, -1, fig) @@ -1026,9 +1022,9 @@ def Destroy(self, *args, **kwargs): class FigureManagerWx(FigureManagerBase): """ - This class contains the FigureCanvas and GUI frame + Container/controller for the FigureCanvas and GUI frame. - It is instantiated by GcfWx whenever a new figure is created. GcfWx is + It is instantiated by Gcf whenever a new figure is created. Gcf is responsible for managing multiple instances of FigureManagerWx. Attributes @@ -1045,8 +1041,16 @@ def __init__(self, canvas, num, frame): self.frame = frame self.window = frame - self.toolmanager = getattr(frame, "toolmanager", None) - self.toolbar = frame.GetToolBar() + @property + def toolbar(self): + return self.frame.GetToolBar() + + @toolbar.setter + def toolbar(self, value): + # Never allow this, except that base class inits this to None before + # the frame is set up. + if value is not None or hasattr(self, "frame"): + raise AttributeError("can't set attribute") def show(self): # docstring inherited @@ -1058,7 +1062,9 @@ def show(self): def destroy(self, *args): # docstring inherited _log.debug("%s - destroy()", type(self)) - self.frame.Close() + frame = self.frame + if frame: # Else, may have been already deleted, e.g. when closing. + frame.Close() wxapp = wx.GetApp() if wxapp: wxapp.Yield() @@ -1079,25 +1085,16 @@ def resize(self, width, height): def _load_bitmap(filename): """ - Load a bitmap file from the backends/images subdirectory in which the - matplotlib library is installed. The filename parameter should not - contain any path information as this is determined automatically. - - Returns a wx.Bitmap object. + Load a wx.Bitmap from a file in the "images" directory of the Matplotlib + data. """ - path = cbook._get_data_path('images', filename) - if not path.exists(): - raise IOError(f"Could not find bitmap file '{path}'; dying") - return wx.Bitmap(str(path)) + return wx.Bitmap(str(cbook._get_data_path('images', filename))) def _set_frame_icon(frame): bundle = wx.IconBundle() for image in ('matplotlib.png', 'matplotlib_large.png'): - try: - icon = wx.Icon(_load_bitmap(image)) - except IOError: - continue + icon = wx.Icon(_load_bitmap(image)) if not icon.IsOk(): return bundle.AddIcon(icon) @@ -1117,7 +1114,6 @@ class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar): def __init__(self, canvas): wx.ToolBar.__init__(self, canvas.GetParent(), -1) NavigationToolbar2.__init__(self, canvas) - self.canvas = canvas self._idle = True self.prevZoomRect = None # for now, use alternate zoom-rectangle drawing on all @@ -1142,7 +1138,7 @@ def _init_toolbar(self): self.wx_ids[text] = ( self.AddTool( -1, - bitmap=_load_bitmap(image_file + ".png"), + bitmap=_load_bitmap(f"{image_file}.png"), bmpDisabled=wx.NullBitmap, label=text, shortHelp=text, longHelp=tooltip_text, kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"] @@ -1216,7 +1212,7 @@ def set_cursor(self, cursor): self.canvas.Update() def press(self, event): - if self._active == 'ZOOM': + if self.mode.name == 'ZOOM': if not self.retinaFix: self.wxoverlay = wx.Overlay() else: @@ -1228,7 +1224,7 @@ def press(self, event): self.zoomAxes = event.inaxes def release(self, event): - if self._active == 'ZOOM': + if self.mode.name == 'ZOOM': # When the mouse is released we reset the overlay and it # restores the former content to the window. if not self.retinaFix: @@ -1323,8 +1319,7 @@ def set_history_buttons(self): class StatusBarWx(wx.StatusBar): """ A status bar is added to _FigureFrame to allow measurements and the - previously selected scroll function to be displayed as a user - convenience. + previously selected scroll function to be displayed as a user convenience. """ def __init__(self, parent, *args, **kwargs): @@ -1354,7 +1349,7 @@ def add_toolitem(self, name, group, position, image_file, description, tool = self.InsertTool(idx, -1, name, bmp, wx.NullBitmap, kind, description or "") else: - size = (self.GetTextExtent(name)[0]+10, -1) + size = (self.GetTextExtent(name)[0] + 10, -1) if toggle: control = wx.ToggleButton(self, -1, name, size=size) else: diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index eb494dd67ca2..d2c93f2b2502 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -67,7 +67,7 @@ def _setup_pyqt5(): global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \ - _isdeleted, _getSaveFileName + _isdeleted, _devicePixelRatio, _setDevicePixelRatio, _getSaveFileName if QT_API == QT_API_PYQT5: from PyQt5 import QtCore, QtGui, QtWidgets @@ -88,10 +88,14 @@ def _isdeleted(obj): return not shiboken2.isValid(obj) def is_pyqt5(): return True + # self.devicePixelRatio() returns 0 in rare cases + def _devicePixelRatio(obj): return obj.devicePixelRatio() or 1 + def _setDevicePixelRatio(obj, factor): obj.setDevicePixelRatio(factor) + def _setup_pyqt4(): global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \ - _isdeleted, _getSaveFileName + _isdeleted, _devicePixelRatio, _setDevicePixelRatio, _getSaveFileName def _setup_pyqt4_internal(api): global QtCore, QtGui, QtWidgets, \ @@ -143,6 +147,9 @@ def _isdeleted(obj): return not shiboken.isValid(obj) def is_pyqt5(): return False + def _devicePixelRatio(obj): return 1 + def _setDevicePixelRatio(obj, factor): pass + if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]: _setup_pyqt5() diff --git a/lib/matplotlib/backends/qt_editor/_formlayout.py b/lib/matplotlib/backends/qt_editor/_formlayout.py index 92a079992ff1..be34e19bad42 100644 --- a/lib/matplotlib/backends/qt_editor/_formlayout.py +++ b/lib/matplotlib/backends/qt_editor/_formlayout.py @@ -218,7 +218,7 @@ def __init__(self, data, comment="", with_margin=False, parent=None): The data to be edited in the form. comment : str, optional - with_margin : bool, optional, default: False + with_margin : bool, default: False If False, the form elements reach to the border of the widget. This is the desired behavior if the FormWidget is used as a widget alongside with other widgets such as a QComboBox, which also do diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index f885568c24d5..962a7f94cdc3 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -75,7 +75,8 @@ def cmp_key(label): curves = [] def prepare_data(d, init): - """Prepare entry for FormLayout. + """ + Prepare entry for FormLayout. *d* is a mapping of shorthands to style names (a single style may have multiple shorthands, in particular the shorthands `None`, @@ -169,7 +170,7 @@ def prepare_data(d, init): datalist.append((mappables, "Images, etc.", "")) def apply_callback(data): - """This function will be called to apply changes""" + """A callback to apply changes.""" orig_xlim = axes.get_xlim() orig_ylim = axes.get_ylim() diff --git a/lib/matplotlib/backends/web_backend/.eslintrc.js b/lib/matplotlib/backends/web_backend/.eslintrc.js new file mode 100644 index 000000000000..f073e7fa3dc0 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + root: true, + ignorePatterns: ["jquery-ui-*/", "node_modules/"], + env: { + browser: true, + jquery: true, + }, + extends: ["eslint:recommended", "prettier"], + globals: { + IPython: "readonly", + MozWebSocket: "readonly", + }, + rules: { + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + }, + ], + }, +}; diff --git a/lib/matplotlib/backends/web_backend/.prettierignore b/lib/matplotlib/backends/web_backend/.prettierignore new file mode 100644 index 000000000000..06a29c66e0fc --- /dev/null +++ b/lib/matplotlib/backends/web_backend/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ + +# Vendored dependencies +css/boilerplate.css +css/fbm.css +css/page.css +jquery-ui-*/ diff --git a/lib/matplotlib/backends/web_backend/.prettierrc b/lib/matplotlib/backends/web_backend/.prettierrc new file mode 100644 index 000000000000..fe8d711065d6 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/.prettierrc @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": "js/**/*.js", + "options": { + "singleQuote": true, + "tabWidth": 4, + } + } + ] +} diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 7c774b4a16d0..4986680f5f2b 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -1,34 +1,36 @@ /* Put everything inside the global mpl namespace */ +/* global mpl */ window.mpl = {}; - -mpl.get_websocket_type = function() { - if (typeof(WebSocket) !== 'undefined') { +mpl.get_websocket_type = function () { + if (typeof WebSocket !== 'undefined') { return WebSocket; - } else if (typeof(MozWebSocket) !== 'undefined') { + } else if (typeof MozWebSocket !== 'undefined') { return MozWebSocket; } else { - alert('Your browser does not have WebSocket support. ' + - 'Please try Chrome, Safari or Firefox ≥ 6. ' + - 'Firefox 4 and 5 are also supported but you ' + - 'have to enable WebSockets in about:config.'); - }; -} + alert( + 'Your browser does not have WebSocket support. ' + + 'Please try Chrome, Safari or Firefox ≥ 6. ' + + 'Firefox 4 and 5 are also supported but you ' + + 'have to enable WebSockets in about:config.' + ); + } +}; -mpl.figure = function(figure_id, websocket, ondownload, parent_element) { +mpl.figure = function (figure_id, websocket, ondownload, parent_element) { this.id = figure_id; this.ws = websocket; - this.supports_binary = (this.ws.binaryType != undefined); + this.supports_binary = this.ws.binaryType !== undefined; if (!this.supports_binary) { - var warnings = document.getElementById("mpl-warnings"); + var warnings = document.getElementById('mpl-warnings'); if (warnings) { warnings.style.display = 'block'; - warnings.textContent = ( - "This browser does not support binary websocket messages. " + - "Performance may be slow."); + warnings.textContent = + 'This browser does not support binary websocket messages. ' + + 'Performance may be slow.'; } } @@ -44,7 +46,7 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.image_mode = 'full'; this.root = $('
'); - this._root_extra_style(this.root) + this._root_extra_style(this.root); this.root.attr('style', 'display: inline-block'); $(parent_element).append(this.root); @@ -57,58 +59,53 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.waiting = false; - this.ws.onopen = function () { - fig.send_message("supports_binary", {value: fig.supports_binary}); - fig.send_message("send_image_mode", {}); - if (mpl.ratio != 1) { - fig.send_message("set_dpi_ratio", {'dpi_ratio': mpl.ratio}); - } - fig.send_message("refresh", {}); + this.ws.onopen = function () { + fig.send_message('supports_binary', { value: fig.supports_binary }); + fig.send_message('send_image_mode', {}); + if (mpl.ratio !== 1) { + fig.send_message('set_dpi_ratio', { dpi_ratio: mpl.ratio }); } + fig.send_message('refresh', {}); + }; - this.imageObj.onload = function() { - if (fig.image_mode == 'full') { - // Full images could contain transparency (where diff images - // almost always do), so we need to clear the canvas so that - // there is no ghosting. - fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height); - } - fig.context.drawImage(fig.imageObj, 0, 0); - }; + this.imageObj.onload = function () { + if (fig.image_mode === 'full') { + // Full images could contain transparency (where diff images + // almost always do), so we need to clear the canvas so that + // there is no ghosting. + fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height); + } + fig.context.drawImage(fig.imageObj, 0, 0); + }; - this.imageObj.onunload = function() { + this.imageObj.onunload = function () { fig.ws.close(); - } + }; this.ws.onmessage = this._make_on_message_function(this); this.ondownload = ondownload; -} +}; -mpl.figure.prototype._init_header = function() { +mpl.figure.prototype._init_header = function () { var titlebar = $( '
'); + 'ui-helper-clearfix"/>' + ); var titletext = $( '
'); - titlebar.append(titletext) + 'text-align: center; padding: 3px;"/>' + ); + titlebar.append(titletext); this.root.append(titlebar); this.header = titletext[0]; -} - - - -mpl.figure.prototype._canvas_extra_style = function(canvas_div) { - -} - +}; -mpl.figure.prototype._root_extra_style = function(canvas_div) { +mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {}; -} +mpl.figure.prototype._root_extra_style = function (_canvas_div) {}; -mpl.figure.prototype._init_canvas = function() { +mpl.figure.prototype._init_canvas = function () { var fig = this; var canvas_div = $('
'); @@ -121,47 +118,53 @@ mpl.figure.prototype._init_canvas = function() { canvas_div.keydown('key_press', canvas_keyboard_event); canvas_div.keyup('key_release', canvas_keyboard_event); - this.canvas_div = canvas_div - this._canvas_extra_style(canvas_div) + this.canvas_div = canvas_div; + this._canvas_extra_style(canvas_div); this.root.append(canvas_div); var canvas = $(''); canvas.addClass('mpl-canvas'); - canvas.attr('style', "left: 0; top: 0; z-index: 0; outline: 0") + canvas.attr('style', 'left: 0; top: 0; z-index: 0; outline: 0'); this.canvas = canvas[0]; - this.context = canvas[0].getContext("2d"); + this.context = canvas[0].getContext('2d'); - var backingStore = this.context.backingStorePixelRatio || - this.context.webkitBackingStorePixelRatio || - this.context.mozBackingStorePixelRatio || - this.context.msBackingStorePixelRatio || - this.context.oBackingStorePixelRatio || - this.context.backingStorePixelRatio || 1; + var backingStore = + this.context.backingStorePixelRatio || + this.context.webkitBackingStorePixelRatio || + this.context.mozBackingStorePixelRatio || + this.context.msBackingStorePixelRatio || + this.context.oBackingStorePixelRatio || + this.context.backingStorePixelRatio || + 1; mpl.ratio = (window.devicePixelRatio || 1) / backingStore; var rubberband = $(''); - rubberband.attr('style', "position: absolute; left: 0; top: 0; z-index: 1;") + rubberband.attr( + 'style', + 'position: absolute; left: 0; top: 0; z-index: 1;' + ); var pass_mouse_events = true; canvas_div.resizable({ - start: function(event, ui) { + start: function (_event, _ui) { pass_mouse_events = false; }, - resize: function(event, ui) { + resize: function (_event, ui) { fig.request_resize(ui.size.width, ui.size.height); }, - stop: function(event, ui) { + stop: function (_event, ui) { pass_mouse_events = true; fig.request_resize(ui.size.width, ui.size.height); }, }); function mouse_event_fn(event) { - if (pass_mouse_events) + if (pass_mouse_events) { return fig.mouse_event(event, event['data']); + } } rubberband.mousedown('button_press', mouse_event_fn); @@ -172,9 +175,9 @@ mpl.figure.prototype._init_canvas = function() { rubberband.mouseenter('figure_enter', mouse_event_fn); rubberband.mouseleave('figure_leave', mouse_event_fn); - canvas_div.on("wheel", function (event) { + canvas_div.on('wheel', function (event) { event = event.originalEvent; - event['data'] = 'scroll' + event['data'] = 'scroll'; if (event.deltaY < 0) { event.step = 1; } else { @@ -188,41 +191,44 @@ mpl.figure.prototype._init_canvas = function() { this.rubberband = rubberband; this.rubberband_canvas = rubberband[0]; - this.rubberband_context = rubberband[0].getContext("2d"); - this.rubberband_context.strokeStyle = "#000000"; + this.rubberband_context = rubberband[0].getContext('2d'); + this.rubberband_context.strokeStyle = '#000000'; - this._resize_canvas = function(width, height) { + this._resize_canvas = function (width, height) { // Keep the size of the canvas, canvas container, and rubber band // canvas in synch. - canvas_div.css('width', width) - canvas_div.css('height', height) + canvas_div.css('width', width); + canvas_div.css('height', height); canvas.attr('width', width * mpl.ratio); canvas.attr('height', height * mpl.ratio); - canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;'); + canvas.attr( + 'style', + 'width: ' + width + 'px; height: ' + height + 'px;' + ); rubberband.attr('width', width); rubberband.attr('height', height); - } + }; // Set the figure to an initial 600x600px, this will subsequently be updated // upon first draw. this._resize_canvas(600, 600); // Disable right mouse context menu. - $(this.rubberband_canvas).bind("contextmenu",function(e){ + $(this.rubberband_canvas).bind('contextmenu', function (_e) { return false; }); - function set_focus () { + function set_focus() { canvas.focus(); canvas_div.focus(); } window.setTimeout(set_focus, 100); -} +}; -mpl.figure.prototype._init_toolbar = function() { +mpl.figure.prototype._init_toolbar = function () { var fig = this; var nav_element = $('
'); @@ -237,7 +243,7 @@ mpl.figure.prototype._init_toolbar = function() { return fig.toolbar_button_onmouseover(event['data']); } - for(var toolbar_ind in mpl.toolbar_items) { + for (var toolbar_ind in mpl.toolbar_items) { var name = mpl.toolbar_items[toolbar_ind][0]; var tooltip = mpl.toolbar_items[toolbar_ind][1]; var image = mpl.toolbar_items[toolbar_ind][2]; @@ -248,8 +254,10 @@ mpl.figure.prototype._init_toolbar = function() { continue; } var button = $(''); + button = $( + '' + ); button.click(method_name, toolbar_event); button.mouseover(tooltip, toolbar_mouse_event); nav_element.append(button); } // Add the status bar. - var status_bar = $(''); + var status_bar = $( + '' + ); nav_element.append(status_bar); this.message = status_bar[0]; // Add the close button to the window. var buttongrp = $('
'); - var button = $(''); - button.click(function (evt) { fig.handle_close(fig, {}); } ); + button = $( + '' + ); + button.click(function (_evt) { + fig.handle_close(fig, {}); + }); button.mouseover('Stop Interaction', toolbar_mouse_event); buttongrp.append(button); var titlebar = this.root.find($('.ui-dialog-titlebar')); titlebar.prepend(buttongrp); -} +}; -mpl.figure.prototype._root_extra_style = function(el){ - var fig = this - el.on("remove", function(){ - fig.close_ws(fig, {}); +mpl.figure.prototype._root_extra_style = function (el) { + var fig = this; + el.on('remove', function () { + fig.close_ws(fig, {}); }); -} +}; -mpl.figure.prototype._canvas_extra_style = function(el){ +mpl.figure.prototype._canvas_extra_style = function (el) { // this is important to make the div 'focusable - el.attr('tabindex', 0) + el.attr('tabindex', 0); // reach out to IPython and tell the keyboard manager to turn it's self // off when our div gets focus // location in version 3 if (IPython.notebook.keyboard_manager) { IPython.notebook.keyboard_manager.register_events(el); - } - else { + } else { // location in version 2 IPython.keyboard_manager.register_events(el); } +}; -} - -mpl.figure.prototype._key_event_extra = function(event, name) { +mpl.figure.prototype._key_event_extra = function (event, _name) { var manager = IPython.notebook.keyboard_manager; - if (!manager) + if (!manager) { manager = IPython.keyboard_manager; + } // Check for shift+enter - if (event.shiftKey && event.which == 13) { + if (event.shiftKey && event.which === 13) { this.canvas_div.blur(); // select the cell after this one var index = IPython.notebook.find_cell_index(this.cell_info[0]); IPython.notebook.select(index + 1); } -} +}; -mpl.figure.prototype.handle_save = function(fig, msg) { +mpl.figure.prototype.handle_save = function (fig, _msg) { fig.ondownload(fig, null); -} - +}; -mpl.find_output_cell = function(html_output) { +mpl.find_output_cell = function (html_output) { // Return the cell and output element which can be found *uniquely* in the notebook. // Note - this is a bit hacky, but it is done because the "notebook_saving.Notebook" // IPython event is triggered only after the cells have been serialised, which for // our purposes (turning an active figure into a static one), is too late. var cells = IPython.notebook.get_cells(); var ncells = cells.length; - for (var i=0; i= 3 moved mimebundle to data attribute of output data = data.data; } - if (data['text/html'] == html_output) { + if (data['text/html'] === html_output) { return [cell, data, j]; } } } } -} +}; // Register the function which deals with the matplotlib target/channel. // The kernel may be null if the page has been refreshed. -if (IPython.notebook.kernel != null) { - IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm); +if (IPython.notebook.kernel !== null) { + IPython.notebook.kernel.comm_manager.register_target( + 'matplotlib', + mpl.mpl_figure_comm + ); } diff --git a/lib/matplotlib/backends/web_backend/package.json b/lib/matplotlib/backends/web_backend/package.json new file mode 100644 index 000000000000..e2a4009a971b --- /dev/null +++ b/lib/matplotlib/backends/web_backend/package.json @@ -0,0 +1,15 @@ +{ + "devDependencies": { + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.1", + "prettier": "^2.0.2" + }, + "scripts": { + "eslint": "eslint . --fix", + "eslint:check": "eslint .", + "lint": "npm run prettier && npm run eslint", + "lint:check": "npm run prettier:check && npm run eslint:check", + "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"", + "prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"" + } +} diff --git a/lib/matplotlib/blocking_input.py b/lib/matplotlib/blocking_input.py index 615b31baf172..a1ec2849f41c 100644 --- a/lib/matplotlib/blocking_input.py +++ b/lib/matplotlib/blocking_input.py @@ -1,6 +1,5 @@ """ -This provides several classes used for blocking interaction with figure -windows: +Classes used for blocking interaction with figure windows: `BlockingInput` Creates a callable object to retrieve events in a blocking way for @@ -235,7 +234,7 @@ def pop_click(self, event, index=-1): def pop(self, event, index=-1): """ - Removes a click and the associated event from the list of clicks. + Remove a click and the associated event from the list of clicks. Defaults to the last click. """ diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 9e0edcb06ea1..1ac0869340fb 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -165,8 +165,7 @@ def __getstate__(self): return {**vars(self), "callbacks": {}, "_func_cid_map": {}} def connect(self, s, func): - """Register *func* to be called when signal *s* is generated. - """ + """Register *func* to be called when signal *s* is generated.""" self._func_cid_map.setdefault(s, {}) try: proxy = weakref.WeakMethod(func, self._remove_proxy) @@ -197,8 +196,7 @@ def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing): del self._func_cid_map[signal] def disconnect(self, cid): - """Disconnect the callback registered with callback id *cid*. - """ + """Disconnect the callback registered with callback id *cid*.""" for eventname, callbackd in list(self.callbacks.items()): try: del callbackd[cid] @@ -760,9 +758,7 @@ def recurse(obj, start, all, current_path): class Grouper: """ - This class provides a lightweight way to group arbitrary objects - together into disjoint sets when a full-blown graph data structure - would be overkill. + A disjoint-set data structure. Objects can be joined using :meth:`join`, tested for connectedness using :meth:`joined`, and all disjoint sets can be retrieved by @@ -770,30 +766,30 @@ class Grouper: The objects being joined must be hashable and weak-referenceable. - For example: - - >>> from matplotlib.cbook import Grouper - >>> class Foo: - ... def __init__(self, s): - ... self.s = s - ... def __repr__(self): - ... return self.s - ... - >>> a, b, c, d, e, f = [Foo(x) for x in 'abcdef'] - >>> grp = Grouper() - >>> grp.join(a, b) - >>> grp.join(b, c) - >>> grp.join(d, e) - >>> sorted(map(tuple, grp)) - [(a, b, c), (d, e)] - >>> grp.joined(a, b) - True - >>> grp.joined(a, c) - True - >>> grp.joined(a, d) - False - + Examples + -------- + >>> from matplotlib.cbook import Grouper + >>> class Foo: + ... def __init__(self, s): + ... self.s = s + ... def __repr__(self): + ... return self.s + ... + >>> a, b, c, d, e, f = [Foo(x) for x in 'abcdef'] + >>> grp = Grouper() + >>> grp.join(a, b) + >>> grp.join(b, c) + >>> grp.join(d, e) + >>> sorted(map(tuple, grp)) + [(a, b, c), (d, e)] + >>> grp.joined(a, b) + True + >>> grp.joined(a, c) + True + >>> grp.joined(a, d) + False """ + def __init__(self, init=()): self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init} @@ -1284,10 +1280,7 @@ def _to_unmasked_float_array(x): def _check_1d(x): - """ - Converts a sequence of less than 1 dimension, to an array of 1 - dimension; leaves everything else untouched. - """ + """Convert scalars to 1d arrays; pass-through arrays as is.""" if not hasattr(x, 'shape') or len(x.shape) < 1: return np.atleast_1d(x) else: diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index fc5dacbf7cb8..f3787c11bba0 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -150,11 +150,12 @@ def get_cmap(name=None, lut=None): class ScalarMappable: """ - This is a mixin class to support scalar data to RGBA mapping. - The ScalarMappable makes use of data normalization before returning - RGBA colors from the given colormap. + A mixin class to map scalar data to RGBA. + The ScalarMappable applies data normalization before returning RGBA colors + from the given colormap. """ + def __init__(self, norm=None, cmap=None): """ @@ -270,7 +271,8 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): return rgba def set_array(self, A): - """Set the image array from numpy array *A*. + """ + Set the image array from numpy array *A*. Parameters ---------- diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index eab47a160fa0..1586f983c6bb 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -917,8 +917,10 @@ def get_paths(self): def legend_elements(self, prop="colors", num="auto", fmt=None, func=lambda x: x, **kwargs): """ - Creates legend handles and labels for a PathCollection. This is useful - for obtaining a legend for a `~.Axes.scatter` plot. E.g.:: + Create legend handles and labels for a PathCollection. + + This is useful for obtaining a legend for a `~.Axes.scatter` plot; + e.g.:: scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3]) plt.legend(*scatter.legend_elements()) @@ -1118,7 +1120,7 @@ def set_verts(self, verts, closed=True): set_paths = set_verts def set_verts_and_codes(self, verts, codes): - """This allows one to initialize vertices with path codes.""" + """Initialize vertices with path codes.""" if len(verts) != len(codes): raise ValueError("'codes' must be a 1D list or array " "with the same length of 'verts'") @@ -1299,7 +1301,7 @@ def __init__(self, segments, # Can be None. antialiaseds : sequence, optional A sequence of ones or zeros. - linestyles : str or tuple, optional + linestyles : str or tuple, default: 'solid' Either one of {'solid', 'dashed', 'dashdot', 'dotted'}, or a dash tuple. The dash tuple is:: @@ -1849,7 +1851,7 @@ def set_paths(self): @staticmethod def convert_mesh_to_paths(tri): """ - Converts a given mesh into a sequence of `~.Path` objects. + Convert a given mesh into a sequence of `~.Path` objects. This function is primarily of use to implementers of backends that do not directly support meshes. @@ -1942,7 +1944,7 @@ def get_datalim(self, transData): @staticmethod def convert_mesh_to_paths(meshWidth, meshHeight, coordinates): """ - Converts a given mesh into a sequence of `~.Path` objects. + Convert a given mesh into a sequence of `~.Path` objects. This function is primarily of use to implementers of backends that do not directly support quadmeshes. @@ -1963,7 +1965,7 @@ def convert_mesh_to_paths(meshWidth, meshHeight, coordinates): def convert_mesh_to_triangles(self, meshWidth, meshHeight, coordinates): """ - Converts a given mesh into a sequence of triangles, each point + Convert a given mesh into a sequence of triangles, each point with its own color. This is useful for experiments using `~.RendererBase.draw_gouraud_triangle`. """ diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 0520ba5cb0ff..8e250b14f030 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -297,8 +297,6 @@ class _ColorbarLogLocator(ticker.LogLocator): """ def __init__(self, colorbar, *args, **kwargs): """ - _ColorbarLogLocator(colorbar, *args, **kwargs) - This ticker needs to know the *colorbar* so that it can access its *vmin* and *vmax*. Otherwise it is the same as `~.ticker.LogLocator`. The ``*args`` and ``**kwargs`` are the diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 86e369f9950a..d461768d737d 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -361,7 +361,7 @@ def to_hex(c, keep_alpha=False): class ColorConverter: """ - This class is only kept for backwards compatibility. + A class only kept for backwards compatibility. Its functionality is entirely provided by module-level functions. """ @@ -1316,7 +1316,7 @@ def _inv_transform(self, a): return a def _transform_vmin_vmax(self): - """Calculates vmin and vmax in the transformed system.""" + """Calculate vmin and vmax in the transformed system.""" vmin, vmax = self.vmin, self.vmax arr = np.array([vmax, vmin]).astype(float) self._upper, self._lower = self._transform(arr) @@ -1687,7 +1687,7 @@ def direction(self): def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.): """ - Calculates the illumination intensity for a surface using the defined + Calculate the illumination intensity for a surface using the defined azimuth and elevation for the light source. This computes the normal vectors for the surface, and then passes them @@ -1741,7 +1741,7 @@ def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.): def shade_normals(self, normals, fraction=1.): """ - Calculates the illumination intensity for the normal vectors of a + Calculate the illumination intensity for the normal vectors of a surface using the defined azimuth and elevation for the light source. Imagine an artificial sun placed at infinity in some azimuth and @@ -1955,9 +1955,9 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, An MxNx3 RGB array of floats ranging from 0 to 1 (color image). intensity : ndarray An MxNx1 array of floats ranging from 0 to 1 (grayscale image). - hsv_max_sat : number, optional, default: 1 + hsv_max_sat : number, default: 1 The maximum saturation value that the *intensity* map can shift the - output image to. Defaults to 1. + output image to. hsv_min_sat : number, optional The minimum saturation value that the *intensity* map can shift the output image to. Defaults to 0. @@ -2008,8 +2008,8 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, def blend_soft_light(self, rgb, intensity): """ - Combines an rgb image with an intensity map using "soft light" - blending. Uses the "pegtop" formula. + Combine an rgb image with an intensity map using "soft light" blending, + using the "pegtop" formula. Parameters ---------- diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 40c2195c48a1..40a1d2b0f62d 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1,5 +1,5 @@ """ -These are classes to support contour plotting and labelling for the Axes class. +Classes to support contour plotting and labelling for the Axes class. """ from numbers import Integral @@ -261,7 +261,7 @@ def set_label_props(self, label, text, color): label.set_text(text) label.set_color(color) label.set_fontproperties(self.labelFontProps) - label.set_clip_box(self.ax.bbox) + label.set_clip_box(self.axes.bbox) def get_text(self, lev, fmt): """Get the text of the label.""" @@ -313,21 +313,22 @@ def locate_label(self, linecontour, labelwidth): def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5): """ - This function calculates the appropriate label rotation given - the linecontour coordinates in screen units, the index of the - label location and the label width. + Calculate the appropriate label rotation given the linecontour + coordinates in screen units, the index of the label location and the + label width. - It will also break contour and calculate inlining if *lc* is - not empty (lc defaults to the empty list if None). *spacing* - is the space around the label in pixels to leave empty. + If *lc* is not None or empty, also break contours and compute + inlining. - Do both of these tasks at once to avoid calculating path lengths + *spacing* is the empty space to leave around the label, in pixels. + + Both tasks are done together to avoid calculating path lengths multiple times, which is relatively costly. - The method used here involves calculating the path length - along the contour in pixel coordinates and then looking - approximately label width / 2 away from central point to - determine rotation and then to break contour if desired. + The method used here involves computing the path length along the + contour in pixel coordinates and then looking approximately (label + width / 2) away from central point to determine rotation and then to + break contour if desired. """ if lc is None: @@ -401,7 +402,7 @@ def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5): return rotation, nlc def _get_label_text(self, x, y, rotation): - dx, dy = self.ax.transData.inverted().transform((x, y)) + dx, dy = self.axes.transData.inverted().transform((x, y)) t = text.Text(dx, dy, rotation=rotation, horizontalalignment='center', verticalalignment='center', zorder=self._clabel_zorder) @@ -412,7 +413,7 @@ def _get_label_clabeltext(self, x, y, rotation): # the data coordinate and create a label using ClabelText # class. This way, the rotation of the clabel is along the # contour line always. - transDataInv = self.ax.transData.inverted() + transDataInv = self.axes.transData.inverted() dx, dy = transDataInv.transform((x, y)) drotation = transDataInv.transform_angles(np.array([rotation]), np.array([[x, y]])) @@ -432,7 +433,7 @@ def _add_label(self, t, x, y, lev, cvalue): self.labelXYs.append((x, y)) # Add label to plot here - useful for manual mode label selection - self.ax.add_artist(t) + self.axes.add_artist(t) def add_label(self, x, y, rotation, lev, cvalue): """ @@ -476,7 +477,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, """ if transform is None: - transform = self.ax.transData + transform = self.axes.transData if transform: x, y = transform.transform((x, y)) @@ -495,7 +496,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, # grab its vertices lc = active_path.vertices # sort out where the new vertex should be added data-units - xcmin = self.ax.transData.inverted().transform([xmin, ymin]) + xcmin = self.axes.transData.inverted().transform([xmin, ymin]) # if there isn't a vertex close enough if not np.allclose(xcmin, lc[imin]): # insert new data into the vertex list @@ -511,13 +512,13 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, lc = paths[segmin].vertices # In pixel/screen space - slc = self.ax.transData.transform(lc) + slc = self.axes.transData.transform(lc) # Get label width for rotating labels and breaking contours lw = self.get_label_width(self.labelLevelList[lmin], self.labelFmt, self.labelFontSizeList[lmin]) # lw is in points. - lw *= self.ax.figure.dpi / 72.0 # scale to screen coordinates + lw *= self.axes.figure.dpi / 72 # scale to screen coordinates # now lw in pixels # Figure out label rotation. @@ -556,7 +557,7 @@ def labels(self, inline, inline_spacing): con = self.collections[icon] trans = con.get_transform() lw = self.get_label_width(lev, self.labelFmt, fsize) - lw *= self.ax.figure.dpi / 72.0 # scale to screen coordinates + lw *= self.axes.figure.dpi / 72 # scale to screen coordinates additions = [] paths = con.get_paths() for segNum, linepath in enumerate(paths): @@ -773,7 +774,7 @@ def __init__(self, ax, *args, Keyword arguments are as described in the docstring of `~.Axes.contour`. """ - self.ax = ax + self.axes = ax self.levels = levels self.filled = filled self.linewidths = linewidths @@ -889,7 +890,7 @@ def __init__(self, ax, *args, alpha=self.alpha, transform=self.get_transform(), zorder=self._contour_zorder) - self.ax.add_collection(col, autolim=False) + self.axes.add_collection(col, autolim=False) self.collections.append(col) else: tlinewidths = self._process_linewidths() @@ -911,14 +912,14 @@ def __init__(self, ax, *args, transform=self.get_transform(), zorder=self._contour_zorder) col.set_label('_nolegend_') - self.ax.add_collection(col, autolim=False) + self.axes.add_collection(col, autolim=False) self.collections.append(col) for col in self.collections: col.sticky_edges.x[:] = [self._mins[0], self._maxs[0]] col.sticky_edges.y[:] = [self._mins[1], self._maxs[1]] - self.ax.update_datalim([self._mins, self._maxs]) - self.ax.autoscale_view(tight=True) + self.axes.update_datalim([self._mins, self._maxs]) + self.axes.autoscale_view(tight=True) self.changed() # set the colors @@ -927,16 +928,21 @@ def __init__(self, ax, *args, cbook._warn_external('The following kwargs were not used by ' 'contour: ' + s) + @cbook.deprecated("3.3") + @property + def ax(self): + return self.axes + def get_transform(self): """ Return the :class:`~matplotlib.transforms.Transform` instance used by this ContourSet. """ if self._transform is None: - self._transform = self.ax.transData + self._transform = self.axes.transData elif (not isinstance(self._transform, mtransforms.Transform) and hasattr(self._transform, '_as_mpl_transform')): - self._transform = self._transform._as_mpl_transform(self.ax) + self._transform = self._transform._as_mpl_transform(self.axes) return self._transform def __getstate__(self): @@ -1431,9 +1437,9 @@ def _process_args(self, *args, corner_mask=None, **kwargs): # if the transform is not trans data, and some part of it # contains transData, transform the xs and ys to data coordinates - if (t != self.ax.transData and - any(t.contains_branch_seperately(self.ax.transData))): - trans_to_data = t - self.ax.transData + if (t != self.axes.transData and + any(t.contains_branch_seperately(self.axes.transData))): + trans_to_data = t - self.axes.transData pts = np.vstack([x.flat, y.flat]).T transformed_pts = trans_to_data.transform(pts) x = transformed_pts[..., 0] @@ -1498,9 +1504,9 @@ def _check_xyz(self, args, kwargs): convert them to 2D using meshgrid. """ x, y = args[:2] - kwargs = self.ax._process_unit_info(xdata=x, ydata=y, kwargs=kwargs) - x = self.ax.convert_xunits(x) - y = self.ax.convert_yunits(y) + kwargs = self.axes._process_unit_info(xdata=x, ydata=y, kwargs=kwargs) + x = self.axes.convert_xunits(x) + y = self.axes.convert_yunits(y) x = np.asarray(x, dtype=np.float64) y = np.asarray(y, dtype=np.float64) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 0ca6a43b5e4a..185f7dfa213d 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -563,7 +563,7 @@ def __call__(self, x, pos=0): class ConciseDateFormatter(ticker.Formatter): """ - This class attempts to figure out the best format to use for the + A `.Formatter` which attempts to figure out the best format to use for the date, and to make it as compact as possible, but still be complete. This is most useful when used with the `AutoDateLocator`:: @@ -761,8 +761,8 @@ def format_data_short(self, value): class AutoDateFormatter(ticker.Formatter): """ - This class attempts to figure out the best format to use. This is - most useful when used with the `AutoDateLocator`. + A `.Formatter` which attempts to figure out the best format to use. This + is most useful when used with the `AutoDateLocator`. The AutoDateFormatter has a scale dictionary that maps the scale of the tick (the distance in days between one major tick) and a @@ -991,9 +991,7 @@ def set_tzinfo(self, tz): self.tz = tz def datalim_to_dt(self): - """ - Convert axis data interval to datetime objects. - """ + """Convert axis data interval to datetime objects.""" dmin, dmax = self.axis.get_data_interval() if dmin > dmax: dmin, dmax = dmax, dmin @@ -1006,9 +1004,7 @@ def datalim_to_dt(self): return num2date(dmin, self.tz), num2date(dmax, self.tz) def viewlim_to_dt(self): - """ - Converts the view interval to datetime objects. - """ + """Convert the view interval to datetime objects.""" vmin, vmax = self.axis.get_view_interval() if vmin > vmax: vmin, vmax = vmax, vmin diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index d96dfb8a7f21..47cc8109ff63 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -274,6 +274,12 @@ def _output(self): maxx = max(maxx, x + w) maxy = max(maxy, y + e) maxy_pure = max(maxy_pure, y) + if self._baseline_v is not None: + maxy_pure = self._baseline_v # This should normally be the case. + self._baseline_v = None + + if not self.text and not self.boxes: # Avoid infs/nans from inf+/-inf. + return Page(text=[], boxes=[], width=0, height=0, descent=0) if self.dpi is None: # special case for ease of debugging: output raw dvi coordinates @@ -301,9 +307,24 @@ def _read(self): Read one page from the file. Return True if successful, False if there were no more pages. """ + # Pages appear to start with the sequence + # bop (begin of page) + # xxx comment + # down + # push + # down, down + # push + # down (possibly multiple) + # push <= here, v is the baseline position. + # etc. + # (dviasm is useful to explore this structure.) + self._baseline_v = None while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) + if (self._baseline_v is None + and len(getattr(self, "stack", [])) == 3): + self._baseline_v = self.v if byte == 140: # end of page return True if self.state is _dvistate.post_post: # end of file @@ -933,9 +954,9 @@ def _parse(self, file): @cbook.deprecated("3.3") class Encoding: 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 this class understands is a very limited subset of PostScript. Usage (subject to change):: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 299e893993f8..bf54af53dfc4 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1204,7 +1204,7 @@ def add_axes(self, *args, **kwargs): fig.add_axes(ax) """ - if not len(args): + if not len(args) and 'rect' not in kwargs: cbook.warn_deprecated( "3.3", message="Calling add_axes() without argument is " diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 292fe8c29a51..910f9363d9ab 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -176,7 +176,7 @@ def win32FontDirectory(): def _win32RegistryFonts(reg_domain, base_dir): r""" - Searches for fonts in the Windows registry. + Search for fonts in the Windows registry. Parameters ---------- @@ -275,8 +275,7 @@ def _call_fc_list(): def get_fontconfig_fonts(fontext='ttf'): - """List the font filenames known to `fc-list` having the given extension. - """ + """List font filenames known to `fc-list` having the given extension.""" fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)] return [fname for fname in _call_fc_list() if Path(fname).suffix.lower() in fontext] @@ -498,7 +497,7 @@ def afmFontProperty(fontpath, font): @cbook.deprecated("3.2", alternative="FontManager.addfont") def createFontList(fontfiles, fontext='ttf'): """ - A function to create a font lookup list. The default is to create + Create a font lookup list. The default is to create a list of TrueType fonts. An AFM font list can optionally be created. """ diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 844ad81fcaab..b2d7c1c8806a 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -48,9 +48,9 @@ def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None): self.set_width_ratios(width_ratios) def __repr__(self): - height_arg = (', height_ratios=%r' % self._row_height_ratios + height_arg = (', height_ratios=%r' % (self._row_height_ratios,) if self._row_height_ratios is not None else '') - width_arg = (', width_ratios=%r' % self._col_width_ratios + width_arg = (', width_ratios=%r' % (self._col_width_ratios,) if self._col_width_ratios is not None else '') return '{clsname}({nrows}, {ncols}{optionals})'.format( clsname=self.__class__.__name__, @@ -462,8 +462,7 @@ def __init__(self, nrows, ncols, artist=self) def get_subplot_params(self, figure=None): - """Return a dictionary of subplot layout parameters. - """ + """Return a dictionary of subplot layout parameters.""" hspace = (self._hspace if self._hspace is not None else figure.subplotpars.hspace if figure is not None else rcParams["figure.subplot.hspace"]) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 7c77936b0702..a83a6b267b70 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1639,7 +1639,7 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', thus supports a wide range of file formats, including PNG, JPG, TIFF and others. - .. _Pillow: http://python-pillow.org/ + .. _Pillow: https://python-pillow.org/ thumbfile : str or file-like The thumbnail filename. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 3e7ee14e25af..a5fbf1937bf3 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -726,7 +726,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): cbook._warn_external( "Legend does not support {!r} instances.\nA proxy artist " "may be used instead.\nSee: " - "http://matplotlib.org/users/legend_guide.html" + "https://matplotlib.org/users/legend_guide.html" "#creating-artists-specifically-for-adding-to-the-legend-" "aka-proxy-artists".format(orig_handle)) # We don't have a handle for this artist, so we just defer @@ -1163,6 +1163,10 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): # One argument. User defined labels - automatic handle detection. elif len(args) == 1: labels, = args + if any(isinstance(l, Artist) for l in labels): + raise TypeError("A single argument passed to legend() must be a " + "list of labels, but found an Artist in there.") + # Get as many handles as there are labels. handles = [handle for handle, label in zip(_get_legend_handles(axs, handlers), labels)] diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 76789d68ff2b..b836a6eef93f 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -1,5 +1,5 @@ """ -This module defines default legend handlers. +Default legend handlers. It is strongly encouraged to have read the :doc:`legend guide ` before this documentation. @@ -21,7 +21,6 @@ derived from the base class (HandlerBase) with the following method:: def legend_artist(self, legend, orig_handle, fontsize, handlebox) - """ from itertools import cycle @@ -628,8 +627,8 @@ def create_artists(self, legend, orig_handle, def _copy_collection_props(self, legend_handle, orig_handle): """ - Method to copy properties from a LineCollection (orig_handle) to a - Line2D (legend_handle). + Copy properties from the `.LineCollection` *orig_handle* to the + `.Line2D` *legend_handle*. """ legend_handle.set_color(orig_handle.get_color()[0]) legend_handle.set_linestyle(orig_handle.get_linestyle()[0]) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 9e90a354c76e..c13cc16ea44e 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1,6 +1,6 @@ """ -This module contains all the 2D line class which can draw with a -variety of line styles, markers and colors. +The 2D line class which can draw with a variety of line styles, markers and +colors. """ # TODO: expose cap and join style attrs @@ -31,8 +31,7 @@ def _get_dash_pattern(style): - """Convert linestyle -> dash pattern - """ + """Convert linestyle to dash pattern.""" # go from short hand -> full strings if isinstance(style, str): style = ls_mapper.get(style, style) @@ -495,7 +494,8 @@ def get_pickradius(self): return self._pickradius def set_pickradius(self, d): - """Set the pick radius used for containment tests. + """ + Set the pick radius used for containment tests. See `.contains` for more details. diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index ccbdce01116b..afa8c105e3c4 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -1,7 +1,6 @@ r""" -This module contains functions to handle markers. Used by both the -marker functionality of `~matplotlib.axes.Axes.plot` and -`~matplotlib.axes.Axes.scatter`. +Functions to handle markers; used by the marker functionality of +`~matplotlib.axes.Axes.plot` and `~matplotlib.axes.Axes.scatter`. All possible markers are defined here: @@ -213,11 +212,11 @@ def __init__(self, marker=None, fillstyle=None): Parameters ---------- - marker : str or array-like, optional, default: None + marker : str or array-like, default: None See the descriptions of possible markers in the module docstring. - fillstyle : str, optional, default: 'full' - 'full', 'left", 'right', 'bottom', 'top', 'none' + fillstyle : str, default: 'full' + One of 'full', 'left', 'right', 'bottom', 'top', 'none'. """ self._marker_function = None self.set_fillstyle(fillstyle) diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 42a75c792c82..586c4d0a75da 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -2276,9 +2276,9 @@ def raise_error(s, loc, toks): class Parser: """ - This is the pyparsing-based parser for math expressions. It - actually parses full strings *containing* math expressions, in - that raw text may also appear outside of pairs of ``$``. + A pyparsing-based parser for strings containing math expressions. + + Raw text may also appear outside of pairs of ``$``. The grammar is based directly on that in TeX, though it cuts a few corners. """ diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index c204edfc4eb4..f24035fc0cdf 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -406,9 +406,8 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, mode=None): """ - This is a helper function that implements the commonality between the - psd, csd, spectrogram and complex, magnitude, angle, and phase spectrums. - It is *NOT* meant to be used outside of mlab and may change at any time. + Private helper implementing the common parts between the psd, csd, + spectrogram and complex, magnitude, angle, and phase spectrums. """ if y is None: # if y is None use x for y @@ -564,9 +563,8 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, def _single_spectrum_helper( mode, x, Fs=None, window=None, pad_to=None, sides=None): """ - This is a helper function that implements the commonality between the - complex, magnitude, angle, and phase spectrums. - It is *NOT* meant to be used outside of mlab and may change at any time. + Private helper implementing the commonality between the complex, magnitude, + angle, and phase spectrums. """ cbook._check_in_list(['complex', 'magnitude', 'angle', 'phase'], mode=mode) @@ -640,7 +638,7 @@ def _single_spectrum_helper( choose one of the functions: 'none' calls `.detrend_none`. 'mean' calls `.detrend_mean`. 'linear' calls `.detrend_linear`. -scale_by_freq : bool, optional, default: True +scale_by_freq : bool, default: True Whether the resulting density values should be scaled by the scaling frequency, which gives density in units of Hz^-1. This allows for integration over the returned frequency values. The default is True for @@ -850,7 +848,7 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap : int, optional The number of points of overlap between blocks. The default value is 128. - mode : str, optional, default: 'psd' + mode : str, default: 'psd' What sort of spectrum to use: 'psd' Returns the power spectral density. @@ -1065,7 +1063,8 @@ def silverman_factor(self): covariance_factor = scotts_factor def evaluate(self, points): - """Evaluate the estimated pdf on a set of points. + """ + Evaluate the estimated pdf on a set of points. Parameters ---------- diff --git a/lib/matplotlib/mpl-data/images/help.gif b/lib/matplotlib/mpl-data/images/help.gif new file mode 100644 index 0000000000000000000000000000000000000000..614fa617ed4fe0b99ca3e5bd1f1018a1b4599c1f GIT binary patch literal 564 zcmZ?wbhEHblwgox_{IPNjEs!T%*^cU>|9)2+}zy!{QN>fLc+qr;^N{83JOX}N-8QU z8X6ipIywdh21Z6kmX?-QR#rAPHnz65etv!d0Rd4_Q86(w$;rtnDJj|6*}1v7`T6+; z1qFqLg(W2=m6es%)zvjMHI0ppZEbCxot=Gsef|CYlO|1?I(6!d88c?hnl*Rs+>IMI zZrZeI^XAQa_wL=lfB(UQ2ag^-diLzubLY;TKY#w_&6~Gx-+uV;;p4}TU%q_#>eZ{) zuV24;^X9{c51&7O{`vFguV24@|Ni~w&!2z){{8>|AMCRsKq&rX0lQKMM1takfqi0w zdsA~uYg>CqXIFPmZ(o16y|t~&1a}t$34U%qaf7M74zjEaOP8|BJM_3HEL*o!X+ftc z`!aq}-esJo-41G7rKea5Ez|8aP_l1w5nHA+&2sTlc9RJPJnLjP^_X(+Qd!lnyo|?i zn!D^$X0_+;rdnD$)^9b1ghUM9N-ooQEA{8C9gjx9gPS{L4U^8ixOgs5fx#L8yP>F~ literal 0 HcmV?d00001 diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index ef5dfa8e3f2a..91e3ed29f28e 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -155,7 +155,7 @@ mathtext.sf : sans\-serif mathtext.fontset : cm # Should be 'cm' (Computer Modern), 'stix', # 'stixsans' or 'custom' mathtext.fallback: cm # Select fallback font from ['cm' (Computer Modern), 'stix' - # 'stixsans'] when a symbol can not be found in one of the + # 'stixsans'] when a symbol can not be found in one of the # custom math fonts. Select 'None' to not perform fallback # and replace the missing character by a dummy. @@ -175,6 +175,7 @@ axes.grid : False # display grid or not axes.grid.which : major axes.grid.axis : both axes.titlesize : large # fontsize of the axes title +axes.titley : 1.0 # at the top, no autopositioning. axes.titlepad : 5.0 # pad between axes and title in points axes.titleweight : normal # font weight for axes title axes.labelsize : medium # fontsize of the x any y labels diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index d7b326bb01a8..539d970f5443 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -470,7 +470,8 @@ def get_capstyle(self): return self._capstyle def set_joinstyle(self, s): - """Set the joinstyle. + """ + Set the joinstyle. Parameters ---------- @@ -705,8 +706,21 @@ def draw(self, renderer): class Rectangle(Patch): """ - A rectangle with lower left at *xy* = (*x*, *y*) with - specified *width*, *height* and rotation *angle*. + A rectangle defined via an anchor point *xy* and its *width* and *height*. + + The rectangle extends from ``xy[0]`` to ``xy[0] + width`` in x-direction + and from ``xy[1]`` to ``xy[1] + height`` in y-direction. :: + + : +------------------+ + : | | + : height | + : | | + : (xy)---- width -----+ + + One may picture *xy* as the bottom left corner, but which corner *xy* is + actually depends on the the direction of the axis and the sign of *width* + and *height*; e.g. *xy* would be the bottom right corner if the x-axis + was inverted or if *width* was negative. """ def __str__(self): @@ -720,22 +734,18 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs): Parameters ---------- xy : (float, float) - The rectangle extends from xy[0] to xy[0] + width in - x-direction and from xy[1] to xy[1] + height in y-direction. + The anchor point. width : float - Rectangle width + Rectangle width. height : float - Rectangle height + Rectangle height. angle : float, default: 0 Rotation in degrees anti-clockwise about *xy*. - fill : bool, default: True - Whether to fill the rectangle. - Notes - ----- - Valid keyword arguments are: - - %(Patch)s + Other Parameters + ---------------- + **kwargs : `.Patch` properties + %(Patch)s """ Patch.__init__(self, **kwargs) @@ -1377,7 +1387,7 @@ def __init__(self, xy, width, height, angle=0, **kwargs): Total length (diameter) of horizontal axis. height : float Total length (diameter) of vertical axis. - angle : scalar, optional + angle : float, default: 0 Rotation in degrees anti-clockwise. Notes @@ -1570,7 +1580,7 @@ def __init__(self, xy, width, height, angle=0.0, angle : float Rotation of the ellipse in degrees (counterclockwise). - theta1, theta2 : float, optional + theta1, theta2 : float, default: 0, 360 Starting and ending angles of the arc in degrees. These values are relative to *angle*, e.g. if *angle* = 45 and *theta1* = 90 the absolute starting angle is 135. @@ -1612,7 +1622,7 @@ def draw(self, renderer): Lancaster, Don. *Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines.* - http://www.tinaja.com/glib/ellipse4.pdf + https://www.tinaja.com/glib/ellipse4.pdf There is a use case where very large ellipses must be drawn with very high accuracy, and it is too expensive to render the @@ -1741,7 +1751,7 @@ def segment_circle_intersect(x0, y0, x1, y1): def bbox_artist(artist, renderer, props=None, fill=True): """ - This is a debug function to draw a rectangle around the bounding + A debug function to draw a rectangle around the bounding box returned by an artist's `.Artist.get_window_extent` to test whether the artist is returning the correct bbox. @@ -1772,7 +1782,7 @@ def bbox_artist(artist, renderer, props=None, fill=True): def draw_bbox(bbox, renderer, color='k', trans=None): """ - This is a debug function to draw a rectangle around the bounding + A debug function to draw a rectangle around the bounding box returned by an artist's `.Artist.get_window_extent` to test whether the artist is returning the correct bbox. """ @@ -2454,15 +2464,12 @@ def _shrink(self, path, shrinkA, shrinkB): def __call__(self, posA, posB, shrinkA=2., shrinkB=2., patchA=None, patchB=None): """ - Calls the *connect* method to create a path between *posA* - and *posB*. The path is clipped and shrunken. + Call the *connect* method to create a path between *posA* and + *posB*; then clip and shrink the path. """ - path = self.connect(posA, posB) - clipped_path = self._clip(path, patchA, patchB) shrunk_path = self._shrink(clipped_path, shrinkA, shrinkB) - return shrunk_path @_register_style(_style_list) @@ -4198,7 +4205,8 @@ def __init__(self, xyA, xyB, coordsA, coordsB=None, clip_on=False, dpi_cor=1., **kwargs): - """Connect point *xyA* in *coordsA* with point *xyB* in *coordsB* + """ + Connect point *xyA* in *coordsA* with point *xyB* in *coordsB*. Valid keys are diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 36eb3f13ac12..9725db239960 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -359,9 +359,10 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None, snap=False, stroke_width=1.0, simplify=None, curves=True, sketch=None): """ - Iterates over all of the curve segments in the path. Each iteration - returns a 2-tuple ``(vertices, code)``, where ``vertices`` is a - sequence of 1-3 coordinate pairs, and ``code`` is a `Path` code. + Iterate over all curve segments in the path. + + Each iteration returns a pair ``(vertices, code)``, where ``vertices`` + is a sequence of 1-3 coordinate pairs, and ``code`` is a `Path` code. Additionally, this method can provide a number of standard cleanups and conversions to the path. @@ -379,7 +380,7 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None, defining a rectangle in which to clip the path. snap : None or bool, optional If True, snap all nodes to pixels; if False, don't snap them. - If None, perform snapping if the path contains only segments + If None, snap if the path contains only segments parallel to the x or y axes, and no more than 1024 of them. stroke_width : float, optional The width of the stroke being drawn (used for path snapping). @@ -522,7 +523,7 @@ def contains_path(self, path, transform=None): Return whether this (closed) path completely contains the given path. If *transform* is not ``None``, the path will be transformed before - performing the test. + checking for containment. """ if transform is not None: transform = transform.frozen() @@ -733,7 +734,7 @@ def circle(cls, center=(0., 0.), radius=1., readonly=False): The circle is approximated using 8 cubic Bezier curves, as described in Lancaster, Don. `Approximating a Circle or an Ellipse Using Four - Bezier Cubic Splines `_. + Bezier Cubic Splines `_. """ MAGIC = 0.2652031 SQRTHALF = np.sqrt(0.5) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index e032d22ffe19..c3c5e5d8b571 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -393,7 +393,7 @@ def transform_non_affine(self, xy): # docstring inherited x, y = xy.T # from Equations (7, 8) of - # http://mathworld.wolfram.com/MollweideProjection.html + # https://mathworld.wolfram.com/MollweideProjection.html theta = np.arcsin(y / np.sqrt(2)) longitude = (np.pi / (2 * np.sqrt(2))) * x / np.cos(theta) latitude = np.arcsin((2 * theta + np.sin(2 * theta)) / np.pi) diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 007308fb4933..be9ebed2ab7b 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -443,6 +443,7 @@ def pan(self, numsteps): def zoom(self, direction): return self.base.zoom(direction) + @cbook.deprecated("3.3") def refresh(self): # docstring inherited return self.base.refresh() @@ -1063,13 +1064,15 @@ def get_theta_offset(self): def set_theta_zero_location(self, loc, offset=0.0): """ - Sets the location of theta's zero. (Calls set_theta_offset - with the correct value in radians under the hood.) + Set the location of theta's zero. + + This simply calls `set_theta_offset` with the correct value in radians. + Parameters + ---------- loc : str May be one of "N", "NW", "W", "SW", "S", "SE", "E", or "NE". - - offset : float, optional + offset : float, default: 0 An offset in degrees to apply from the specified *loc*. **Note:** this offset is *always* applied counter-clockwise regardless of the direction setting. @@ -1258,7 +1261,8 @@ def get_rlabel_position(self): return np.rad2deg(self._r_label_position.get_matrix()[0, 2]) def set_rlabel_position(self, value): - """Updates the theta position of the radius labels. + """ + Update the theta position of the radius labels. Parameters ---------- diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2a48a9a68262..2ea56557989f 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -626,7 +626,7 @@ def figure(num=None, # autoincrement if None, else integer from 1-N def _auto_draw_if_interactive(fig, val): """ - This is an internal helper function for making sure that auto-redrawing + An internal helper function for making sure that auto-redrawing works as intended in the plain python repl. Parameters @@ -1702,13 +1702,6 @@ def thetagrids(*args, **kwargs): return lines, labels -## Plotting Info ## - - -def plotting(): - pass - - def get_plot_commands(): """ Get a sorted list of all of the plotting commands. @@ -1718,7 +1711,7 @@ def get_plot_commands(): # functions, and anything marked as private with a preceding underscore. exclude = {'colormaps', 'colors', 'connect', 'disconnect', 'get_plot_commands', 'get_current_fig_manager', 'ginput', - 'plotting', 'waitforbuttonpress'} + 'waitforbuttonpress'} exclude |= set(colormaps()) this_module = inspect.getmodule(get_plot_commands) return sorted( @@ -1779,7 +1772,7 @@ def colormaps(): ========= =================================================== The following colormaps are based on the `ColorBrewer - `_ color specifications and designs developed by + `_ color specifications and designs developed by Cynthia Brewer: ColorBrewer Diverging (luminance is highest at the midpoint, and @@ -1955,7 +1948,7 @@ def colormaps(): .. [#] Rainbow colormaps, ``jet`` in particular, are considered a poor choice for scientific visualization by many researchers: `Rainbow Color Map (Still) Considered Harmful - `_ + `_ .. [#] Resembles "BkBlAqGrYeOrReViWh200" from NCAR Command Language. See `Color Table Gallery @@ -1974,7 +1967,7 @@ def colormaps(): def _setup_pyplot_info_docstrings(): """ - Generates the plotting docstring. + Generate the plotting docstring. These must be done after the entire module is imported, so it is called from the end of this module, which is generated by @@ -2931,9 +2924,9 @@ def sci(im): # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.set_title) -def title(label, fontdict=None, loc=None, pad=None, **kwargs): +def title(label, fontdict=None, loc=None, pad=None, *, y=None, **kwargs): return gca().set_title( - label, fontdict=fontdict, loc=loc, pad=pad, **kwargs) + label, fontdict=fontdict, loc=loc, pad=pad, y=y, **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index abed5202cbb8..daff61d0c35c 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -259,7 +259,7 @@ def __init__(self, Q, X, Y, U, label, self.color = color self.label = label self._labelsep_inches = labelsep - self.labelsep = (self._labelsep_inches * Q.ax.figure.dpi) + self.labelsep = (self._labelsep_inches * Q.axes.figure.dpi) # try to prevent closure over the real self weak_self = weakref.ref(self) @@ -272,8 +272,8 @@ def on_dpi_change(fig): # the start of draw. self_weakref._initialized = False - self._cid = Q.ax.figure.callbacks.connect('dpi_changed', - on_dpi_change) + self._cid = Q.axes.figure.callbacks.connect( + 'dpi_changed', on_dpi_change) self.labelpos = labelpos self.labelcolor = labelcolor @@ -293,13 +293,10 @@ def on_dpi_change(fig): self.zorder = Q.zorder + 0.1 def remove(self): - """ - Overload the remove method - """ - self.Q.ax.figure.callbacks.disconnect(self._cid) + # docstring inherited + self.Q.axes.figure.callbacks.disconnect(self._cid) self._cid = None - # pass the remove call up the stack - martist.Artist.remove(self) + super().remove() # pass the remove call up the stack def _init(self): if True: # not self._initialized: @@ -355,16 +352,12 @@ def draw(self, renderer): self.stale = False def _set_transform(self): - if self.coord == 'data': - self.set_transform(self.Q.ax.transData) - elif self.coord == 'axes': - self.set_transform(self.Q.ax.transAxes) - elif self.coord == 'figure': - self.set_transform(self.Q.ax.figure.transFigure) - elif self.coord == 'inches': - self.set_transform(self.Q.ax.figure.dpi_scale_trans) - else: - raise ValueError('unrecognized coordinates') + self.set_transform(cbook._check_getitem({ + "data": self.Q.axes.transData, + "axes": self.Q.axes.transAxes, + "figure": self.Q.axes.figure.transFigure, + "inches": self.Q.axes.figure.dpi_scale_trans, + }, coordinates=self.coord)) def set_figure(self, fig): martist.Artist.set_figure(self, fig) @@ -477,7 +470,7 @@ def __init__(self, ax, *args, by the following pyplot interface documentation: %s """ - self.ax = ax + self._axes = ax # The attr actually set by the Artist.axes property. X, Y, U, V, C = _parse_args(*args, caller_name='quiver()') self.X = X self.Y = Y @@ -510,8 +503,7 @@ def __init__(self, ax, *args, self.set_UVC(U, V, C) self._initialized = False - # try to prevent closure over the real self - weak_self = weakref.ref(self) + weak_self = weakref.ref(self) # Prevent closure over the real self. def on_dpi_change(fig): self_weakref = weak_self() @@ -522,18 +514,17 @@ def on_dpi_change(fig): # the start of draw. self_weakref._initialized = False - self._cid = self.ax.figure.callbacks.connect('dpi_changed', - on_dpi_change) + self._cid = ax.figure.callbacks.connect('dpi_changed', on_dpi_change) + + @cbook.deprecated("3.3", alternative="axes") + def ax(self): + return self.axes def remove(self): - """ - Overload the remove method - """ - # disconnect the call back - self.ax.figure.callbacks.disconnect(self._cid) + # docstring inherited + self.axes.figure.callbacks.disconnect(self._cid) self._cid = None - # pass the remove call up the stack - mcollections.PolyCollection.remove(self) + super().remove() # pass the remove call up the stack def _init(self): """ @@ -544,8 +535,7 @@ def _init(self): # available to have this work on an as-needed basis at present. if True: # not self._initialized: trans = self._set_transform() - ax = self.ax - self.span = trans.inverted().transform_bbox(ax.bbox).width + self.span = trans.inverted().transform_bbox(self.axes.bbox).width if self.width is None: sn = np.clip(math.sqrt(self.N), 8, 25) self.width = 0.06 * self.span / sn @@ -606,38 +596,37 @@ def _dots_per_unit(self, units): """ Return a scale factor for converting from units to pixels """ - ax = self.ax if units in ('x', 'y', 'xy'): if units == 'x': - dx0 = ax.viewLim.width - dx1 = ax.bbox.width + dx0 = self.axes.viewLim.width + dx1 = self.axes.bbox.width elif units == 'y': - dx0 = ax.viewLim.height - dx1 = ax.bbox.height + dx0 = self.axes.viewLim.height + dx1 = self.axes.bbox.height else: # 'xy' is assumed - dxx0 = ax.viewLim.width - dxx1 = ax.bbox.width - dyy0 = ax.viewLim.height - dyy1 = ax.bbox.height + dxx0 = self.axes.viewLim.width + dxx1 = self.axes.bbox.width + dyy0 = self.axes.viewLim.height + dyy1 = self.axes.bbox.height dx1 = np.hypot(dxx1, dyy1) dx0 = np.hypot(dxx0, dyy0) dx = dx1 / dx0 else: if units == 'width': - dx = ax.bbox.width + dx = self.axes.bbox.width elif units == 'height': - dx = ax.bbox.height + dx = self.axes.bbox.height elif units == 'dots': dx = 1.0 elif units == 'inches': - dx = ax.figure.dpi + dx = self.axes.figure.dpi else: raise ValueError('unrecognized units') return dx def _set_transform(self): """ - Sets the PolygonCollection transform to go + Set the PolyCollection transform to go from arrow width units to pixels. """ dx = self._dots_per_unit(self.units) @@ -647,9 +636,9 @@ def _set_transform(self): return trans def _angles_lengths(self, U, V, eps=1): - xy = self.ax.transData.transform(self.XY) + xy = self.axes.transData.transform(self.XY) uv = np.column_stack((U, V)) - xyp = self.ax.transData.transform(self.XY + eps * uv) + xyp = self.axes.transData.transform(self.XY + eps * uv) dxy = xyp - xy angles = np.arctan2(dxy[:, 1], dxy[:, 0]) lengths = np.hypot(*dxy.T) / eps @@ -667,7 +656,7 @@ def _make_verts(self, U, V, angles): # Calculate eps based on the extents of the plot # so that we don't end up with roundoff error from # adding a small number to a large. - eps = np.abs(self.ax.dataLim.extents).max() * 0.001 + eps = np.abs(self.axes.dataLim.extents).max() * 0.001 angles, lengths = self._angles_lengths(U, V, eps=eps) if str_angles and self.scale_units == 'xy': a = lengths @@ -803,7 +792,6 @@ def _h_arrows(self, length): : / \ \ \ : ------------------------------ - The largest increment is given by a triangle (or "flag"). After those come full lines (barbs). The smallest increment is a half line. There is only, of course, ever at most 1 half line. If the magnitude is @@ -815,8 +803,6 @@ def _h_arrows(self, length): See also https://en.wikipedia.org/wiki/Wind_barb. - - Parameters ---------- X, Y : 1D or 2D array-like, optional @@ -1027,48 +1013,48 @@ def _find_tails(self, mag, rounding=True, half=5, full=10, flag=50): def _make_barbs(self, u, v, nflags, nbarbs, half_barb, empty_flag, length, pivot, sizes, fill_empty, flip): """ - This function actually creates the wind barbs. *u* and *v* - are components of the vector in the *x* and *y* directions, - respectively. - - *nflags*, *nbarbs*, and *half_barb*, empty_flag* are, - *respectively, the number of flags, number of barbs, flag for - *half a barb, and flag for empty barb, ostensibly obtained - *from :meth:`_find_tails`. + Create the wind barbs. - *length* is the length of the barb staff in points. - - *pivot* specifies the point on the barb around which the - entire barb should be rotated. Right now, valid options are - 'tip' and 'middle'. Can also be a number, which shifts the start - of the barb that many points from the origin. - - *sizes* is a dictionary of coefficients specifying the ratio - of a given feature to the length of the barb. These features - include: - - - *spacing*: space between features (flags, full/half - barbs) - - - *height*: distance from shaft of top of a flag or full - barb - - - *width* - width of a flag, twice the width of a full barb - - - *emptybarb* - radius of the circle used for low - magnitudes - - *fill_empty* specifies whether the circle representing an - empty barb should be filled or not (this changes the drawing - of the polygon). - - *flip* is a flag indicating whether the features should be flipped to - the other side of the barb (useful for winds in the southern - hemisphere). - - This function returns list of arrays of vertices, defining a polygon - for each of the wind barbs. These polygons have been rotated to - properly align with the vector direction. + Parameters + ---------- + u, v + Components of the vector in the x and y directions, respectively. + + nflags, nbarbs, half_barb, empty_flag + Respectively, the number of flags, number of barbs, flag for + half a barb, and flag for empty barb, ostensibly obtained from + :meth:`_find_tails`. + + length + The length of the barb staff in points. + + pivot : {"tip", "middle"} or number + The point on the barb around which the entire barb should be + rotated. If a number, the start of the barb is shifted by that + many points from the origin. + + sizes : dict + Coefficients specifying the ratio of a given feature to the length + of the barb. These features include: + + - *spacing*: space between features (flags, full/half barbs). + - *height*: distance from shaft of top of a flag or full barb. + - *width*: width of a flag, twice the width of a full barb. + - *emptybarb*: radius of the circle used for low magnitudes. + + fill_empty : bool + Whether the circle representing an empty barb should be filled or + not (this changes the drawing of the polygon). + + flip : list of bool + Whether the features should be flipped to the other side of the + barb (useful for winds in the southern hemisphere). + + Returns + ------- + list of arrays of vertices + Polygon vertices for each of the wind barbs. These polygons have + been rotated to properly align with the vector direction. """ # These control the spacing and size of barb elements relative to the diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 8153000a3df2..513860988b66 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -818,7 +818,7 @@ def validate_hatch(s): def cycler(*args, **kwargs): """ - Creates a `~cycler.Cycler` object much like :func:`cycler.cycler`, + Create a `~cycler.Cycler` object much like :func:`cycler.cycler`, but includes input validation. Call signatures:: @@ -1219,7 +1219,8 @@ def _convert_validator_spec(key, conv): 'axes.titlelocation': ['center', ['left', 'center', 'right']], # alignment of axes title 'axes.titleweight': ['normal', validate_fontweight], # font weight of axes title 'axes.titlecolor': ['auto', validate_color_or_auto], # font color of axes title - 'axes.titlepad': [6.0, validate_float], # pad from axes top to title in points + 'axes.titley': [None, validate_float_or_None], # title location, axes units, None means auto + 'axes.titlepad': [6.0, validate_float], # pad from axes top decoration to title in points 'axes.grid': [False, validate_bool], # display grid or not 'axes.grid.which': ['major', ['minor', 'both', 'major']], # set whether the grid is drawn on # 'major' 'minor' or 'both' ticks diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index b4f43dd00623..e59e644c912d 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -246,7 +246,8 @@ def __init__(self, lines, arrows, **kwargs): # ======================== class DomainMap: - """Map representing different coordinate systems. + """ + Map representing different coordinate systems. Coordinate definitions: @@ -361,7 +362,8 @@ def within_grid(self, xi, yi): class StreamMask: - """Mask to keep track of discrete regions crossed by streamlines. + """ + Mask to keep track of discrete regions crossed by streamlines. The resolution of this grid determines the approximate spacing between trajectories. Streamlines are only allowed to pass through zeroed cells: @@ -396,7 +398,8 @@ def _undo_trajectory(self): self._mask[t] = 0 def _update_trajectory(self, xm, ym): - """Update current trajectory position in mask. + """ + Update current trajectory position in mask. If the new position has already been filled, raise `InvalidIndexError`. """ @@ -446,7 +449,8 @@ def backward_time(xi, yi): return -dxi, -dyi def integrate(x0, y0): - """Return x, y grid-coordinates of trajectory based on starting point. + """ + Return x, y grid-coordinates of trajectory based on starting point. Integrate both forward and backward in time from starting point in grid coordinates. @@ -492,7 +496,8 @@ class OutOfBounds(IndexError): def _integrate_rk12(x0, y0, dmap, f, maxlength): - """2nd-order Runge-Kutta algorithm with adaptive step size. + """ + 2nd-order Runge-Kutta algorithm with adaptive step size. This method is also referred to as the improved Euler's method, or Heun's method. This method is favored over higher-order methods because: @@ -658,7 +663,8 @@ def interpgrid(a, xi, yi): def _gen_starting_points(shape): - """Yield starting points for streamlines. + """ + Yield starting points for streamlines. Trying points on the boundary first gives higher quality streamlines. This algorithm starts with a point on the mask corner and spirals inward. diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 649a9f328952..9cdabb0827dd 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -66,7 +66,8 @@ def _apply_style(d, warn=True): def use(style): - """Use matplotlib style settings from a style specification. + """ + Use Matplotlib style settings from a style specification. The style name of 'default' is reserved for reverting back to the default style settings. @@ -128,7 +129,8 @@ def use(style): @contextlib.contextmanager def context(style, after_reset=False): - """Context manager for using style settings temporarily. + """ + Context manager for using style settings temporarily. Parameters ---------- @@ -204,9 +206,10 @@ def read_style_directory(style_dir): def update_nested_dict(main_dict, new_dict): - """Update nested dict (only level of nesting) with new values. + """ + Update nested dict (only level of nesting) with new values. - Unlike dict.update, this assumes that the values of the parent dict are + Unlike `dict.update`, this assumes that the values of the parent dict are dicts (or dict-like), so you shouldn't replace the nested dict if it already exists. Instead you should update the sub-dict. """ diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index ddb00f47ccee..94337489a882 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -7,7 +7,7 @@ # Copyright The Matplotlib development team """ -This module provides functionality to add a table to a plot. +Tables drawing. Use the factory function `~matplotlib.table.table` to create a ready-made table from texts. If you need more control, use the `.Table` class and its @@ -39,12 +39,21 @@ class Cell(Rectangle): PAD = 0.1 """Padding between text and rectangle.""" + _edges = 'BRTL' + _edge_aliases = {'open': '', + 'closed': _edges, # default + 'horizontal': 'BT', + 'vertical': 'RL' + } + def __init__(self, xy, width, height, edgecolor='k', facecolor='w', fill=True, text='', loc=None, - fontproperties=None + fontproperties=None, + *, + visible_edges='closed', ): """ Parameters @@ -68,20 +77,26 @@ def __init__(self, xy, width, height, fontproperties : dict A dict defining the font properties of the text. Supported keys and values are the keyword arguments accepted by `.FontProperties`. + visible_edges : str, default: 'closed' + The cell edges to be drawn with a line: a substring of 'BRTL' + (bottom, right, top, left), or one of 'open' (no edges drawn), + 'closed' (all edges drawn), 'horizontal' (bottom and top), + 'vertical' (right and left). """ # Call base Rectangle.__init__(self, xy, width=width, height=height, fill=fill, edgecolor=edgecolor, facecolor=facecolor) self.set_clip_on(False) + self.visible_edges = visible_edges # Create text object if loc is None: loc = 'right' self._loc = loc - self._text = Text(x=xy[0], y=xy[1], text=text, - fontproperties=fontproperties) - self._text.set_clip_on(False) + self._text = Text(x=xy[0], y=xy[1], clip_on=False, + text=text, fontproperties=fontproperties, + horizontalalignment=loc, verticalalignment='center') def set_transform(self, trans): Rectangle.set_transform(self, trans) @@ -122,35 +137,24 @@ def draw(self, renderer): return # draw the rectangle Rectangle.draw(self, renderer) - # position the text self._set_text_position(renderer) self._text.draw(renderer) self.stale = False def _set_text_position(self, renderer): - """Set text up so it draws in the right place. - - Currently support 'left', 'center' and 'right' - """ + """Set text up so it is drawn in the right place.""" bbox = self.get_window_extent(renderer) - l, b, w, h = bbox.bounds - - # draw in center vertically - self._text.set_verticalalignment('center') - y = b + (h / 2.0) - - # now position horizontally - if self._loc == 'center': - self._text.set_horizontalalignment('center') - x = l + (w / 2.0) - elif self._loc == 'left': - self._text.set_horizontalalignment('left') - x = l + (w * self.PAD) - else: - self._text.set_horizontalalignment('right') - x = l + (w * (1.0 - self.PAD)) - + # center vertically + y = bbox.y0 + bbox.height / 2 + # position horizontally + loc = self._text.get_horizontalalignment() + if loc == 'center': + x = bbox.x0 + bbox.width / 2 + elif loc == 'left': + x = bbox.x0 + bbox.width * self.PAD + else: # right. + x = bbox.x0 + bbox.width * (1 - self.PAD) self._text.set_position((x, y)) def get_text_bounds(self, renderer): @@ -178,23 +182,6 @@ def set_text_props(self, **kwargs): self._text.update(kwargs) self.stale = True - -class CustomCell(Cell): - """ - A `.Cell` subclass with configurable edge visibility. - """ - - _edges = 'BRTL' - _edge_aliases = {'open': '', - 'closed': _edges, # default - 'horizontal': 'BT', - 'vertical': 'RL' - } - - def __init__(self, *args, visible_edges, **kwargs): - super().__init__(*args, **kwargs) - self.visible_edges = visible_edges - @property def visible_edges(self): """ @@ -239,6 +226,9 @@ def get_path(self): ) +CustomCell = Cell # Backcompat. alias. + + class Table(Artist): """ A table of cells. @@ -342,12 +332,12 @@ def add_cell(self, row, col, *args, **kwargs): Returns ------- - `.CustomCell` + `.Cell` The created cell. """ xy = (0, 0) - cell = CustomCell(xy, visible_edges=self.edges, *args, **kwargs) + cell = Cell(xy, visible_edges=self.edges, *args, **kwargs) self[row, col] = cell return cell @@ -355,7 +345,7 @@ def __setitem__(self, position, cell): """ Set a custom cell in a given position. """ - cbook._check_isinstance(CustomCell, cell=cell) + cbook._check_isinstance(Cell, cell=cell) try: row, col = position[0], position[1] except Exception as err: @@ -374,7 +364,7 @@ def __getitem__(self, position): @property def edges(self): """ - The default value of `~.CustomCell.visible_edges` for newly added + The default value of `~.Cell.visible_edges` for newly added cells using `.add_cell`. Notes @@ -724,7 +714,7 @@ def table(ax, edges : substring of 'BRTL' or {'open', 'closed', 'horizontal', 'vertical'} The cell edges to be drawn with a line. See also - `~.CustomCell.visible_edges`. + `~.Cell.visible_edges`. Returns ------- diff --git a/lib/matplotlib/testing/disable_internet.py b/lib/matplotlib/testing/disable_internet.py index 7ad46cd90f97..c70a0020219b 100644 --- a/lib/matplotlib/testing/disable_internet.py +++ b/lib/matplotlib/testing/disable_internet.py @@ -30,7 +30,7 @@ def check_internet_off(original_function): """ - Wraps ``original_function``, which in most cases is assumed + Wrap ``original_function``, which in most cases is assumed to be a `socket.socket` method, to raise an `IOError` for any operations on non-local AF_INET sockets. """ @@ -135,8 +135,10 @@ def turn_on_internet(verbose=False): @contextlib.contextmanager def no_internet(verbose=False): - """Context manager to temporarily disable internet access (if not already - disabled). If it was already disabled before entering the context manager + """ + Temporarily disables internet access (if not already disabled). + + If it was already disabled before entering the context manager (i.e. `turn_off_internet` was called previously) then this is a no-op and leaves internet access disabled until a manual call to `turn_on_internet`. """ diff --git a/lib/matplotlib/testing/jpl_units/Duration.py b/lib/matplotlib/testing/jpl_units/Duration.py index 87639729bf0b..723cbea1dfcd 100644 --- a/lib/matplotlib/testing/jpl_units/Duration.py +++ b/lib/matplotlib/testing/jpl_units/Duration.py @@ -6,12 +6,13 @@ class Duration: - """Class Duration in development. - """ + """Class Duration in development.""" + allowed = ["ET", "UTC"] def __init__(self, frame, seconds): - """Create a new Duration object. + """ + Create a new Duration object. = ERROR CONDITIONS - If the input frame is not in the allowed list, an error is thrown. @@ -62,7 +63,8 @@ def __ge__(self, rhs): return self._cmp(rhs, operator.ge) def _cmp(self, rhs, op): - """Compare two Durations. + """ + Compare two Durations. = INPUT VARIABLES - rhs The Duration to compare against. @@ -75,7 +77,8 @@ def _cmp(self, rhs, op): return op(self._seconds, rhs._seconds) def __add__(self, rhs): - """Add two Durations. + """ + Add two Durations. = ERROR CONDITIONS - If the input rhs is not in the same frame, an error is thrown. @@ -96,7 +99,8 @@ def __add__(self, rhs): return Duration(self._frame, self._seconds + rhs._seconds) def __sub__(self, rhs): - """Subtract two Durations. + """ + Subtract two Durations. = ERROR CONDITIONS - If the input rhs is not in the same frame, an error is thrown. @@ -111,7 +115,8 @@ def __sub__(self, rhs): return Duration(self._frame, self._seconds - rhs._seconds) def __mul__(self, rhs): - """Scale a UnitDbl by a value. + """ + Scale a UnitDbl by a value. = INPUT VARIABLES - rhs The scalar to multiply by. @@ -122,7 +127,8 @@ def __mul__(self, rhs): return Duration(self._frame, self._seconds * float(rhs)) def __rmul__(self, lhs): - """Scale a Duration by a value. + """ + Scale a Duration by a value. = INPUT VARIABLES - lhs The scalar to multiply by. @@ -141,7 +147,8 @@ def __repr__(self): return "Duration('%s', %g)" % (self._frame, self._seconds) def checkSameFrame(self, rhs, func): - """Check to see if frames are the same. + """ + Check to see if frames are the same. = ERROR CONDITIONS - If the frame of the rhs Duration is not the same as our frame, diff --git a/lib/matplotlib/testing/jpl_units/Epoch.py b/lib/matplotlib/testing/jpl_units/Epoch.py index 9810d3e06d9c..613846b3a06d 100644 --- a/lib/matplotlib/testing/jpl_units/Epoch.py +++ b/lib/matplotlib/testing/jpl_units/Epoch.py @@ -21,7 +21,8 @@ class Epoch: } def __init__(self, frame, sec=None, jd=None, daynum=None, dt=None): - """Create a new Epoch object. + """ + Create a new Epoch object. Build an epoch 1 of 2 ways: @@ -124,7 +125,8 @@ def __ge__(self, rhs): return self._cmp(rhs, operator.ge) def _cmp(self, rhs, op): - """Compare two Epoch's. + """ + Compare two Epoch's. = INPUT VARIABLES - rhs The Epoch to compare against. @@ -143,7 +145,8 @@ def _cmp(self, rhs, op): return op(t._seconds, rhs._seconds) def __add__(self, rhs): - """Add a duration to an Epoch. + """ + Add a duration to an Epoch. = INPUT VARIABLES - rhs The Epoch to subtract. @@ -160,7 +163,8 @@ def __add__(self, rhs): return Epoch(t._frame, sec, t._jd) def __sub__(self, rhs): - """Subtract two Epoch's or a Duration from an Epoch. + """ + Subtract two Epoch's or a Duration from an Epoch. Valid: Duration = Epoch - Epoch @@ -199,7 +203,8 @@ def __repr__(self): @staticmethod def range(start, stop, step): - """Generate a range of Epoch objects. + """ + Generate a range of Epoch objects. Similar to the Python range() method. Returns the range [ start, stop) at the requested step. Each element will be a diff --git a/lib/matplotlib/testing/jpl_units/EpochConverter.py b/lib/matplotlib/testing/jpl_units/EpochConverter.py index 4d7a6c52113e..32926b18bbff 100644 --- a/lib/matplotlib/testing/jpl_units/EpochConverter.py +++ b/lib/matplotlib/testing/jpl_units/EpochConverter.py @@ -19,26 +19,16 @@ class EpochConverter(units.ConversionInterface): @staticmethod def axisinfo(unit, axis): - """: Returns information on how to handle an axis that has Epoch data. - - = INPUT VARIABLES - - unit The units to use for a axis with Epoch data. - - = RETURN VALUE - - Returns a AxisInfo data structure that contains - minor/major formatters, major/minor locators, and default - label information. - """ - + # docstring inherited majloc = date_ticker.AutoDateLocator() majfmt = date_ticker.AutoDateFormatter(majloc) - return units.AxisInfo(majloc=majloc, majfmt=majfmt, label=unit) @staticmethod def float2epoch(value, unit): - """: Convert a Matplotlib floating-point date into an Epoch of the - specified units. + """ + Convert a Matplotlib floating-point date into an Epoch of the specified + units. = INPUT VARIABLES - value The Matplotlib floating-point date. @@ -55,8 +45,9 @@ def float2epoch(value, unit): @staticmethod def epoch2float(value, unit): - """: Convert an Epoch value to a float suitable for plotting as a - python datetime object. + """ + Convert an Epoch value to a float suitable for plotting as a python + datetime object. = INPUT VARIABLES - value An Epoch or list of Epochs that need to be converted. @@ -69,8 +60,9 @@ def epoch2float(value, unit): @staticmethod def duration2float(value): - """: Convert a Duration value to a float suitable for plotting as a - python datetime object. + """ + Convert a Duration value to a float suitable for plotting as a python + datetime object. = INPUT VARIABLES - value A Duration or list of Durations that need to be converted. @@ -82,16 +74,8 @@ def duration2float(value): @staticmethod def convert(value, unit, axis): - """: Convert value using unit to a float. If value is a sequence, return - the converted sequence. + # docstring inherited - = INPUT VARIABLES - - value The value or list of values that need to be converted. - - unit The units to use for an axis with Epoch data. - - = RETURN VALUE - - Returns the value parameter converted to floats. - """ # Delay-load due to circular dependencies. import matplotlib.testing.jpl_units as U @@ -108,14 +92,7 @@ def convert(value, unit, axis): @staticmethod def default_units(value, axis): - """: Return the default unit for value, or None. - - = INPUT VARIABLES - - value The value or list of values that need units. - - = RETURN VALUE - - Returns the default units to use for value. - """ + # docstring inherited if cbook.is_scalar_or_string(value): return value.frame() else: diff --git a/lib/matplotlib/testing/jpl_units/StrConverter.py b/lib/matplotlib/testing/jpl_units/StrConverter.py index 4a0b0db3690c..a47bcdd343a2 100644 --- a/lib/matplotlib/testing/jpl_units/StrConverter.py +++ b/lib/matplotlib/testing/jpl_units/StrConverter.py @@ -8,8 +8,8 @@ class StrConverter(units.ConversionInterface): - """: A matplotlib converter class. Provides matplotlib conversion - functionality for string data values. + """ + A Matplotlib converter class for string data values. Valid units for string are: - 'indexed' : Values are indexed as they are specified for plotting. @@ -20,33 +20,12 @@ class StrConverter(units.ConversionInterface): @staticmethod def axisinfo(unit, axis): - """: Returns information on how to handle an axis that has string data. - - = INPUT VARIABLES - - axis The axis using this converter. - - unit The units to use for a axis with string data. - - = RETURN VALUE - - Returns a matplotlib AxisInfo data structure that contains - minor/major formatters, major/minor locators, and default - label information. - """ - + # docstring inherited return None @staticmethod def convert(value, unit, axis): - """: Convert value using unit to a float. If value is a sequence, return - the converted sequence. - - = INPUT VARIABLES - - axis The axis using this converter. - - value The value or list of values that need to be converted. - - unit The units to use for a axis with Epoch data. - - = RETURN VALUE - - Returns the value parameter converted to floats. - """ + # docstring inherited if units.ConversionInterface.is_numlike(value): return value @@ -116,16 +95,6 @@ def convert(value, unit, axis): @staticmethod def default_units(value, axis): - """: Return the default unit for value, or None. - - = INPUT VARIABLES - - axis The axis using this converter. - - value The value or list of values that need units. - - = RETURN VALUE - - Returns the default units to use for value. - Return the default unit for value, or None. - """ - + # docstring inherited # The default behavior for string indexing. return "indexed" diff --git a/lib/matplotlib/testing/jpl_units/UnitDbl.py b/lib/matplotlib/testing/jpl_units/UnitDbl.py index f8df012d6d68..645bc6f6600e 100644 --- a/lib/matplotlib/testing/jpl_units/UnitDbl.py +++ b/lib/matplotlib/testing/jpl_units/UnitDbl.py @@ -6,8 +6,8 @@ class UnitDbl: - """Class UnitDbl in development. - """ + """Class UnitDbl in development.""" + # Unit conversion table. Small subset of the full one but enough # to test the required functions. First field is a scale factor to # convert the input units to the units of the second field. Only @@ -32,7 +32,8 @@ class UnitDbl: } def __init__(self, value, units): - """Create a new UnitDbl object. + """ + Create a new UnitDbl object. Units are internally converted to km, rad, and sec. The only valid inputs for units are [m, km, mile, rad, deg, sec, min, hour]. @@ -52,7 +53,8 @@ def __init__(self, value, units): self._units = data[1] def convert(self, units): - """Convert the UnitDbl to a specific set of units. + """ + Convert the UnitDbl to a specific set of units. = ERROR CONDITIONS - If the input units are not in the allowed list, an error is thrown. @@ -105,7 +107,8 @@ def __ge__(self, rhs): return self._cmp(rhs, operator.ge) def _cmp(self, rhs, op): - """Compare two UnitDbl's. + """ + Compare two UnitDbl's. = ERROR CONDITIONS - If the input rhs units are not the same as our units, @@ -122,7 +125,8 @@ def _cmp(self, rhs, op): return op(self._value, rhs._value) def __add__(self, rhs): - """Add two UnitDbl's. + """ + Add two UnitDbl's. = ERROR CONDITIONS - If the input rhs units are not the same as our units, @@ -138,7 +142,8 @@ def __add__(self, rhs): return UnitDbl(self._value + rhs._value, self._units) def __sub__(self, rhs): - """Subtract two UnitDbl's. + """ + Subtract two UnitDbl's. = ERROR CONDITIONS - If the input rhs units are not the same as our units, @@ -154,7 +159,8 @@ def __sub__(self, rhs): return UnitDbl(self._value - rhs._value, self._units) def __mul__(self, rhs): - """Scale a UnitDbl by a value. + """ + Scale a UnitDbl by a value. = INPUT VARIABLES - rhs The scalar to multiply by. @@ -165,7 +171,8 @@ def __mul__(self, rhs): return UnitDbl(self._value * rhs, self._units) def __rmul__(self, lhs): - """Scale a UnitDbl by a value. + """ + Scale a UnitDbl by a value. = INPUT VARIABLES - lhs The scalar to multiply by. @@ -189,7 +196,8 @@ def type(self): @staticmethod def range(start, stop, step=None): - """Generate a range of UnitDbl objects. + """ + Generate a range of UnitDbl objects. Similar to the Python range() method. Returns the range [ start, stop) at the requested step. Each element will be a @@ -222,7 +230,8 @@ def range(start, stop, step=None): @cbook.deprecated("3.2") def checkUnits(self, units): - """Check to see if some units are valid. + """ + Check to see if some units are valid. = ERROR CONDITIONS - If the input units are not in the allowed list, an error is thrown. @@ -236,7 +245,8 @@ def checkUnits(self, units): units, list(self.allowed.keys()))) def checkSameUnits(self, rhs, func): - """Check to see if units are the same. + """ + Check to see if units are the same. = ERROR CONDITIONS - If the units of the rhs UnitDbl are not the same as our units, diff --git a/lib/matplotlib/testing/jpl_units/UnitDblConverter.py b/lib/matplotlib/testing/jpl_units/UnitDblConverter.py index 35bf5dd3a105..37734e5ced60 100644 --- a/lib/matplotlib/testing/jpl_units/UnitDblConverter.py +++ b/lib/matplotlib/testing/jpl_units/UnitDblConverter.py @@ -40,16 +40,8 @@ class UnitDblConverter(units.ConversionInterface): @staticmethod def axisinfo(unit, axis): - """: Returns information on how to handle an axis that has Epoch data. + # docstring inherited - = INPUT VARIABLES - - unit The units to use for a axis with Epoch data. - - = RETURN VALUE - - Returns a matplotlib AxisInfo data structure that contains - minor/major formatters, major/minor locators, and default - label information. - """ # Delay-load due to circular dependencies. import matplotlib.testing.jpl_units as U @@ -71,16 +63,7 @@ def axisinfo(unit, axis): @staticmethod def convert(value, unit, axis): - """: Convert value using unit to a float. If value is a sequence, return - the converted sequence. - - = INPUT VARIABLES - - value The value or list of values that need to be converted. - - unit The units to use for a axis with Epoch data. - - = RETURN VALUE - - Returns the value parameter converted to floats. - """ + # docstring inherited if not cbook.is_scalar_or_string(value): return [UnitDblConverter.convert(x, unit, axis) for x in value] # If the incoming value behaves like a number, @@ -99,15 +82,7 @@ def convert(value, unit, axis): @staticmethod def default_units(value, axis): - """: Return the default unit for value, or None. - - = INPUT VARIABLES - - value The value or list of values that need units. - - = RETURN VALUE - - Returns the default units to use for value. - Return the default unit for value, or None. - """ + # docstring inherited # Determine the default units based on the user preferences set for # default units when printing a UnitDbl. if cbook.is_scalar_or_string(value): diff --git a/lib/matplotlib/testing/jpl_units/__init__.py b/lib/matplotlib/testing/jpl_units/__init__.py index 45fa99e0e573..b8caa9a8957a 100644 --- a/lib/matplotlib/testing/jpl_units/__init__.py +++ b/lib/matplotlib/testing/jpl_units/__init__.py @@ -1,6 +1,6 @@ """ -This is a sample set of units for use with testing unit conversion -of matplotlib routines. These are used because they use very strict +A sample set of units for use with testing unit conversion +of Matplotlib routines. These are used because they use very strict enforcement of unitized data which will test the entire spectrum of how unitized data might be used (it is not always meaningful to convert to a float without specific units given). diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_raster.pdf b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_raster.pdf index 2a7484d0b7a12b366f2d924613c04cf37be657c4..5e3b389b1deab0d47b9379f8924166c159b9947f 100644 GIT binary patch literal 7245 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 diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png index 9a92f45e1e182dc670261f78445f7ea620d2362b..e4a9183612f5255bb048c6ffa1667584b6977405 100644 GIT binary patch literal 13512 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 literal 9329 zcmeHNc|6qnzy6ZYW+@?rL#b1uvNpC(St{W=S)wfEh^#q`eVLO^L}Si5ijK9D!<4-& zBf|^{$qz@GCXF#7X^b%@#xi5feb4>*ZyZ@Ai2w&-1*W@AuAsI6Lgv zs3#AN%IjCYG=)!rCv#|iMO38P4is|M35Wlq79BH zCrz}H=C=|UwqN4e4{rV*`P5tn#Z#xkcmIi)~B?%i$gZlmGH4*WK^5dSVb1H zBZotp-X)tL$mQ;sH9>>yUIaO#dlEq~#*$Ls-hPY2w<7q~4BwF88zTM>>0l8Lf}q~( z^|LmLz(fsufV8uvuc$NPMa8aF5ri^&MAGL! zo8ZEEHdT9I&VamK`V3!*3~6>po7k-?p(6CLX~ zF{t5SZZSJUP2bkrDPXA z@r9|zg#-7uy(Q8kZ1p2?C^hWWA4L=_2nrmH0MbxO>G1zm}{S=+)z5BW#>^l)%&kDWVpB%6gQ!!eR7sx!m$5t=ztS z6(iS|1j2GE&c5uyz_np!%yC1Ww}XR&r?cTEWWGmMcHyU1dc@t{sr$#G;^S2n{Rnn@ zR*w`DDc|*PVQ}KB{$ckymIaMUgNEgyta9)qg=dtdO zJkG)4SX6St;^)^x5d8D_5$yZTtS?`J11kLP2ksHaBQp~U`(XcZ>GV$fBkyG}h2k=U z(vD=1uu!_N8pp`P{Xa9wGU-38FLcT0TldDGuv1f0*!T0z`uz`vX4IpODEc|%CARyv ztCFZJI|5Dovx(mxWUK^{&~zfuY9c>)dKLA&4QYaUbf}|d7Z(?ouZ-SP-w0{Ea-qU+ zx1Hh$vr+Ch_*xZfA4)oew5Ur+S{96k#o||>=G3F3$*ZSB zjJy6RnTvmUXFQu-=zMhW5j~=JaL~Ceo_lqN=SpwznWo}nNZ2+B$@)_!!Vrq=nTFB3 zE~VBrZ2uR-!AkT9GdX#A!qo%Ekc<0HA|5TXNcXw2;YSsqnQt}^TH*@G@FE-b41v8#J!5!y@B zwvz=PZ>Vw@WE|iK!{asbu}y`&)LPL zjLPcfa--T(g(yh+6hM^se^r@bxBc#O&6At`A+g%s$=0}3vQ~QML(&~hxeQk&JIHjP zSH#QA(*{hq!Kw- zRmie0d#cD)R77i!#d(kXbo|b6a18V58)nSpK%p7$i~UP#Z@8mwmPY7#LQW9@v2~KuUjCH;-sAB#;es8=6ETC-oASromr=9D|6-Jt0?{36a zR>9O3DixYT91vzq;`eH(;=v}fTa+tdWy8Tty%6_gw^G*P%+RsYq5Sc8VWwpx%(shd z!&Xki65#vt%1YgO8xYfYTS7FGM=>~GYLiA8_R6kVFwq0jJ@}Zo$;F3mQ|sd8rFd}8Ulp0z4Fn)*(aBTsiI*`2Rt6no$k+v z;)|V;^lyqb?Florz-2CuhBePSdR1}blQk82{hghiO&mYrn9gJyCFDqZICrtpyJiuV zR8D~7mam+!u&81{hGIzs0%2)6TPsZ~n|i5TLsQdNa11EJkz=EY(@Ng!`6N{j$8Gn3 zj`7?`H68E~Nz0^nu1$)t9%Ou#W&h{4Qc;Rq3&y8vo#>`((i1{1D)}BJm zXnFUPI*7juA01J~KjCSJ$bU>_f1JD7-Sd1kY=K3st4k3%_)w<&?Yo|U61uOaEK}S``k_v{pFUD^d2n6{rKDVCcjFVE-75&J#@G2CD&D=mQU)14GPobYtVm@s* zqCCQ@PScFDMK<+kg9ef0Y*=a4!7LyPo>33w%$b)|^~#XO$;p9tDVE)`@X_a?LC&DQ zb9d!NN>t-dh5FJIy)=2?9_|#*W}F$|;nehj!N9e*x0h?@tfIaiEq2a7^^6kI4G;i$ z2HmTddluNuLH&0)2cXp~T8q;8Yj)+4>QW-06h%Zjq#*I?in?#J2ipF{q*FM$!ClF2 zeMq$KVrv;D^{G2s#Zg3O@#a$+-US;^Lc-p#dZ)N=680~yS>Eotl9KZRn#rS9@K|5^ zRYeZwUf`a^we@#FVQxXeZf46C78Xwl1kbefcuP`D00ShFX`xS6J!lP8xr}_0JrE0A zg;nqepC)1SOKLZp0et{cD;jI^u7Qf=9sDQRTBJvc#5nCc`Q96-xy|^FRbqkSKR4sH7GKGk1c-0tM!Ssl~s)kkJKVJ(aO9u#clCbQQ-Fem7)@N=7W9q z(C5fm*;jxox|7RKKZKb^7WG6IHfgv&yllCx{9u;3rR7}++`zTP@z4iq*q+x+cbrLa zoqm73Xd@-%-76qcF?x@;C&jTCXstlUMg-2fCcIosak(0c1 zI-gU`;1p(Ut~X$f%RI1o@5!&qPFk)*$$y;m`p&amzl8PlTot92w}!~b$i$(t15698 zzAX0YmUc&7!llT)nq1-Ci{ShC;YXN%mwHA)TaO1D*QS(;#hST8z-CN4bpNCA z_j=y|gphp+G_X?X+x8j?-=hj0=sz&C zSk7suv0?N`xW^30&iJF5#;y^mZ+dU~4pWb={QV}YWKXR!EiZ6wqH9{SvbC@B-R;Ze z-pQ%MVkR#tvYg&;{bV*NHEZ)1&|sur>QYH@JBqXM^dL2uAV71|*alW5{m-i9_l-58 zjX?EgJ!*-<_B)X|Vl9k!DK%&(p~}jDU{W~6&NIWQE|l8{$SV-XAkbHrn7MXL4A0)oSqtZL?=@%|?-04v$?xnVUb>jonodCYcNq zr%(98~kHcH!}i1J|IjYOEJ_iM3}5Hl#DbcK(+*O3vjt9|r|4x@s7(ZDHZY^p{uN2cN{+ ze^AcW)pmJaJa^T0Z289X*=upd>Plf{hvZ7G8rSma?Foz5rfGFs*NKO+9*qkt0sZ^> z`s^d(wl7@X0*CKjqq!Wj`t*oSr^j07X%Qu{&?qH4bP8mp z;%~FU>MMK4#v~RRM8vam;8XJ6GI;nu43)WJIiAdX5cwdrW5*ZHs)`sU237 zZHCJ>ds=nZzO4Mn51gM*bAQ|P3REkIvmVg-puy?!2B)C@k63jj6&6lsZ+ipi=@GW4 zAfbbH_54`lUJmGmXZlX$Ry;Re!_M`x#fQcgtFLzM$@)3-M@lkNqof*u2&}DB7dWAy zLasED$(%8Hj7gNjcj{L_ZuIq?g=yV4@mkY|o3T;8t4!wg6dk@D)rk!ACGYL;L!~x; zt!s?Y(I=&6BMCG@PF7}}*=3lLrk&!NFv-@Xeu+uYn`LEX@r$3GU{Fr{+dt}cUnNsN zTPuzlI`E?~*(hR3Sw#WYI~AIy32bksd99C9xvQ_rNm04$A&OAJ;J~n1Bhph&dxy2x z^=(gdw;>(KJqz?~OJ>xm-1fA1;e;PSEop^BZKax))@-V0OUbH#Tv%B65Y#`jxK^X` z(AP}z=a`rnc0)6Wdl<$tTVlg|+?SV^J#x;AmnT41aU!|4b0Z@oeD4T;i{NtT?L$S! zBbKVz@o)s}NV;C`onf4e*e4-m*WR4xv8ZC{4I1gRFqQIT9xiNfa8O5Qas=-prpfQ! zi9{m$29*An6#@E2HA-d1T6t*4Yz%60mp@i~d7nP*n`T+^tVN#1Xjdy*YGR@{;}-qz z(JpFD`G0e%PmbL?`#Wzz=!QZ_j4+lgi1bF-|{iRknPZO7^o6@Q*lkw$7i= zz&ExuPfcE33fUKO0Y4!=6ft_YovU9onUVIj={&M_UI{3rS9^e5h8v5 zHQN$qanF(!MMbfBf+xN=Nm%Cp?n)_%w#==VEjFv1J6b&V*z z$l1&t$XEvQOXygPT(8rN)8(Za{nQ${I}ozaJv^_by^-KP>IGzm5>Q=TDFDUJFeeA!6SZikBY2>bG5p z>3uL%J}oR9l}`Tw49$N}=2wrZUQRkmzfjdYh0Wwk@tHAd^oY1{YK(f>@MF2VA!>~7 z(;C#Z=*^g3|Ee9pfb+J_XVo3kkP85mC^U8L_=4&%T20nC9D z?IF2wY)OpZsQ7zIY}B z69eABeXz}r!#O4?9a`Yeh=0^Sx_Mi?QhKL$>gvY~$GTEyJV@O+4FyD72b{|@VAz1+ z0+t96=mj!qgqbOCdYk=FLhKOZDn^*^Bl58ql zCGfm}jQClAG$c#`_G^G>fiDeC*s9nL72o)ENA!1UcbtRy2q-!GzG3!(;;srfOby-# z9Pe(p6YWCul?c;5@Ykle*u=!vc}L8Cz-e$8;p|%0$@LQ&{^?u5(ih*e2B&_^3tYU0 z2BT0bq#5#>*YZoirGz=jFqzPAx6%H8+(t|DUfH~m!AKKX>F8{5acR|S=Mdy#d8`GqtAA?*@8n^3MY11}aymc*x$H+GZPzd9zb z(Brr>HlhT2{wAbjGg-8ew7yJ>0NoB#MXvfPsU_I%0z={cJpFQ3V%>V!Z diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 1eb2eda9abe5..828cc1438be7 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3791,8 +3791,7 @@ def test_eventplot_defaults(): ('red', (0, 1, 0), None, (1, 0, 1, 0.5)), # a tricky case mixing types ]) def test_eventplot_colors(colors): - """Test the *colors* parameter of eventplot. Inspired by the issue #8193. - """ + """Test the *colors* parameter of eventplot. Inspired by issue #8193.""" data = [[i] for i in range(4)] # 4 successive events of different nature # Build the list of the expected colors @@ -5522,6 +5521,7 @@ def test_titlesetpos(): def test_title_xticks_top(): # Test that title moves if xticks on top of axes. + mpl.rcParams['axes.titley'] = None fig, ax = plt.subplots() ax.xaxis.set_ticks_position('top') ax.set_title('xlabel top') @@ -5531,6 +5531,7 @@ def test_title_xticks_top(): def test_title_xticks_top_both(): # Test that title moves if xticks on top of axes. + mpl.rcParams['axes.titley'] = None fig, ax = plt.subplots() ax.tick_params(axis="x", bottom=True, top=True, labelbottom=True, labeltop=True) diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index d3443c24e9b0..469b601ce0a7 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -1,7 +1,8 @@ import re from matplotlib.backend_bases import ( - FigureCanvasBase, LocationEvent, RendererBase) + FigureCanvasBase, LocationEvent, MouseButton, MouseEvent, + NavigationToolbar2, RendererBase) import matplotlib.pyplot as plt import matplotlib.transforms as transforms import matplotlib.path as path @@ -99,3 +100,47 @@ def test_location_event_position(x, y): ax.format_coord(x, y)) ax.fmt_xdata = ax.fmt_ydata = lambda x: "foo" assert re.match("x=foo +y=foo", ax.format_coord(x, y)) + + +def test_interactive_zoom(): + fig, ax = plt.subplots() + ax.set(xscale="logit") + + class NT2(NavigationToolbar2): + def _init_toolbar(self): pass + + tb = NT2(fig.canvas) + tb.zoom() + + xlim0 = ax.get_xlim() + ylim0 = ax.get_ylim() + + # Zoom from x=1e-6, y=0.1 to x=1-1e-5, 0.8 (data coordinates, "d"). + d0 = (1e-6, 0.1) + d1 = (1-1e-5, 0.8) + # Convert to screen coordinates ("s"). Events are defined only with pixel + # precision, so round the pixel values, and below, check against the + # corresponding xdata/ydata, which are close but not equal to d0/d1. + s0 = ax.transData.transform(d0).astype(int) + s1 = ax.transData.transform(d1).astype(int) + + # Zoom in. + start_event = MouseEvent( + "button_press_event", fig.canvas, *s0, MouseButton.LEFT) + fig.canvas.callbacks.process(start_event.name, start_event) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *s1, MouseButton.LEFT) + fig.canvas.callbacks.process(stop_event.name, stop_event) + assert ax.get_xlim() == (start_event.xdata, stop_event.xdata) + assert ax.get_ylim() == (start_event.ydata, stop_event.ydata) + + # Zoom out. + start_event = MouseEvent( + "button_press_event", fig.canvas, *s1, MouseButton.RIGHT) + fig.canvas.callbacks.process(start_event.name, start_event) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *s0, MouseButton.RIGHT) + fig.canvas.callbacks.process(stop_event.name, stop_event) + # Absolute tolerance much less than original xmin (1e-7). + assert ax.get_xlim() == pytest.approx(xlim0, rel=0, abs=1e-10) + assert ax.get_ylim() == pytest.approx(ylim0, rel=0, abs=1e-10) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index fabe9878e5fd..e5a6946e2211 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -1,5 +1,6 @@ import importlib import importlib.util +import json import os import signal import subprocess @@ -60,6 +61,7 @@ def _get_testable_interactive_backends(): _test_script = """\ import importlib import importlib.util +import json import sys from unittest import TestCase @@ -70,6 +72,8 @@ def _get_testable_interactive_backends(): "webagg.open_in_browser": False, "webagg.port_retries": 1, }) +if len(sys.argv) >= 2: # Second argument is json-encoded rcParams. + rcParams.update(json.loads(sys.argv[1])) backend = plt.rcParams["backend"].lower() assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises @@ -122,10 +126,14 @@ def check_alt_backend(alt_backend): @pytest.mark.parametrize("backend", _get_testable_interactive_backends()) +@pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) @pytest.mark.flaky(reruns=3) -def test_interactive_backend(backend): +def test_interactive_backend(backend, toolbar): + if backend == "macosx" and toolbar == "toolmanager": + pytest.skip("toolmanager is not implemented for macosx.") proc = subprocess.run( - [sys.executable, "-c", _test_script], + [sys.executable, "-c", _test_script, + json.dumps({"toolbar": toolbar})], env={**os.environ, "MPLBACKEND": backend}, timeout=_test_timeout, stdout=subprocess.PIPE, universal_newlines=True) if proc.returncode: diff --git a/lib/matplotlib/tests/test_category.py b/lib/matplotlib/tests/test_category.py index d7c8df8e21b5..1fcdc3eca59d 100644 --- a/lib/matplotlib/tests/test_category.py +++ b/lib/matplotlib/tests/test_category.py @@ -59,7 +59,8 @@ def __init__(self, units): class TestStrCategoryConverter: - """Based on the pandas conversion and factorization tests: + """ + Based on the pandas conversion and factorization tests: ref: /pandas/tseries/tests/test_converter.py /pandas/tests/test_algos.py:TestFactorize diff --git a/lib/matplotlib/tests/test_compare_images.py b/lib/matplotlib/tests/test_compare_images.py index 4d9d6ef62395..95173a8860a1 100644 --- a/lib/matplotlib/tests/test_compare_images.py +++ b/lib/matplotlib/tests/test_compare_images.py @@ -43,7 +43,8 @@ ('all128.png', 'all127.png', 0, 1), ]) def test_image_comparison_expect_rms(im1, im2, tol, expect_rms): - """Compare two images, expecting a particular RMS error. + """ + Compare two images, expecting a particular RMS error. im1 and im2 are filenames relative to the baseline_dir directory. diff --git a/lib/matplotlib/tests/test_gridspec.py b/lib/matplotlib/tests/test_gridspec.py index fa931ce086e5..5d5d7523a2ed 100644 --- a/lib/matplotlib/tests/test_gridspec.py +++ b/lib/matplotlib/tests/test_gridspec.py @@ -29,3 +29,9 @@ def test_height_ratios(): def test_repr(): ss = gridspec.GridSpec(3, 3)[2, 1:3] assert repr(ss) == "GridSpec(3, 3)[2:3, 1:3]" + + ss = gridspec.GridSpec(2, 2, + height_ratios=(3, 1), + width_ratios=(1, 3)) + assert repr(ss) == \ + "GridSpec(2, 2, height_ratios=(3, 1), width_ratios=(1, 3))" diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index b3868224643d..70494169c504 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -234,6 +234,13 @@ def test_legend_handle_label(self): plt.legend(lines, ['hello world']) Legend.assert_called_with(plt.gca(), lines, ['hello world']) + def test_legend_handles_only(self): + lines = plt.plot(range(10)) + with pytest.raises(TypeError, match='but found an Artist'): + # a single arg is interpreted as labels + # it's a common error to just pass handles + plt.legend(lines) + def test_legend_no_args(self): lines = plt.plot(range(10), label='hello world') with mock.patch('matplotlib.legend.Legend') as Legend: diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index f4b259be3ae4..a348d2fcea01 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -618,6 +618,7 @@ def test_annotation_units(fig_test, fig_ref): def test_large_subscript_title(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams['axes.titley'] = None fig, axs = plt.subplots(1, 2, figsize=(9, 2.5), constrained_layout=True) ax = axs[0] @@ -626,9 +627,7 @@ def test_large_subscript_title(): ax.set_xticklabels('') ax = axs[1] - tt = ax.set_title(r'$\sum_{i} x_i$') - x, y = tt.get_position() - tt.set_position((x, 1.01)) + tt = ax.set_title(r'$\sum_{i} x_i$', y=1.01) ax.set_title('Old Way', loc='left') ax.set_xticklabels('') diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 0114fa50eda5..263df149de5d 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -556,6 +556,16 @@ def test_scilimits(self, sci_type, scilimits, lim, orderOfMag, fewticks): tmp_form.set_locs(ax.yaxis.get_majorticklocs()) assert orderOfMag == tmp_form.orderOfMagnitude + def test_cursor_precision(self): + fig, ax = plt.subplots() + ax.set_xlim(-1, 1) # Pointing precision of 0.001. + fmt = ax.xaxis.get_major_formatter().format_data_short + assert fmt(0.) == "0.000" + assert fmt(0.0123) == "0.012" + assert fmt(0.123) == "0.123" + assert fmt(1.23) == "1.230" + assert fmt(12.3) == "12.300" + class FakeAxis: """Allow Formatter to be called without having a "full" plot set up.""" diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index 674cec00b06b..20ec0bbaa1b0 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -19,7 +19,7 @@ def example_plot(ax, fontsize=12): ax.set_title('Title', fontsize=fontsize) -@image_comparison(['tight_layout1']) +@image_comparison(['tight_layout1'], tol=1.9) def test_tight_layout1(): """Test tight_layout for a single subplot.""" fig, ax = plt.subplots() @@ -115,7 +115,7 @@ def test_tight_layout6(): h_pad=0.45) -@image_comparison(['tight_layout7']) +@image_comparison(['tight_layout7'], tol=1.9) def test_tight_layout7(): # tight layout with left and right titles fontsize = 24 diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index d13ff30060f5..8fccfe4a18b9 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -1,5 +1,3 @@ -import platform - import numpy as np import pytest @@ -12,24 +10,38 @@ pytestmark = pytest.mark.skip('Missing TeX of Ghostscript or dvipng') -@image_comparison(baseline_images=['test_usetex'], - extensions=['pdf', 'png'], - tol={'aarch64': 2.868, 'x86_64': 2.868}.get( - platform.machine(), 0.3 - )) +@image_comparison( + baseline_images=['test_usetex'], + extensions=['pdf', 'png'], + style="mpl20") def test_usetex(): mpl.rcParams['text.usetex'] = True fig = plt.figure() ax = fig.add_subplot(111) - ax.text(0.1, 0.2, + kwargs = {"verticalalignment": "baseline", "size": 24, + "bbox": dict(pad=0, edgecolor="k", facecolor="none")} + ax.text(0.2, 0.7, # the \LaTeX macro exercises character sizing and placement, # \left[ ... \right\} draw some variable-height characters, # \sqrt and \frac draw horizontal rules, \mathrm changes the font r'\LaTeX\ $\left[\int\limits_e^{2e}' r'\sqrt\frac{\log^3 x}{x}\,\mathrm{d}x \right\}$', - fontsize=24) - ax.set_xticks([]) - ax.set_yticks([]) + **kwargs) + ax.text(0.2, 0.3, "lg", **kwargs) + ax.text(0.4, 0.3, r"$\frac{1}{2}\pi$", **kwargs) + ax.text(0.6, 0.3, "$p^{3^A}$", **kwargs) + ax.text(0.8, 0.3, "$p_{3_2}$", **kwargs) + for x in {t.get_position()[0] for t in ax.texts}: + ax.axvline(x) + for y in {t.get_position()[1] for t in ax.texts}: + ax.axhline(y) + ax.set_axis_off() + + +@check_figures_equal() +def test_empty(fig_test, fig_ref): + mpl.rcParams['text.usetex'] = True + fig_test.text(.5, .5, "% a comment") @check_figures_equal() diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0d50389ae35f..bfdbc5e153b8 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -342,7 +342,8 @@ def test_slider_horizontal_vertical(): def check_polygon_selector(event_sequence, expected_result, selections_count): - """Helper function to test Polygon Selector + """ + Helper function to test Polygon Selector. Parameters ---------- diff --git a/lib/matplotlib/tests/tinypages/range6.py b/lib/matplotlib/tests/tinypages/range6.py index d7de4cfc9be4..fa5d035e4ab2 100644 --- a/lib/matplotlib/tests/tinypages/range6.py +++ b/lib/matplotlib/tests/tinypages/range6.py @@ -2,12 +2,12 @@ def range4(): - """This is never be called if plot_directive works as expected.""" + """Never called if plot_directive works as expected.""" raise NotImplementedError def range6(): - """This is the function that should be executed.""" + """The function that should be executed.""" plt.figure() plt.plot(range(6)) plt.show() diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index f954254adc9e..3badbbc21117 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -1,8 +1,7 @@ r""" -This module supports embedded TeX expressions in matplotlib via dvipng -and dvips for the raster and postscript backends. The tex and -dvipng/dvips information is cached in ~/.matplotlib/tex.cache for reuse between -sessions +Support for embedded TeX expressions in Matplotlib via dvipng and dvips for the +raster and PostScript backends. The tex and dvipng/dvips information is cached +in ~/.matplotlib/tex.cache for reuse between sessions. Requirements: @@ -186,6 +185,9 @@ def _get_preamble(self): self._font_preamble, r"\usepackage[utf8]{inputenc}", r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", + # Needs to come early so that the custom preamble can change the + # geometry, e.g. in convert_psfrags. + r"\usepackage[papersize=72in,body=70in,margin=1in]{geometry}", self.get_custom_preamble(), ]) @@ -204,10 +206,10 @@ def make_tex(self, tex, fontsize): Path(texfile).write_text( r""" %s -\usepackage[papersize={72in,72in},body={70in,70in},margin={1in,1in}]{geometry} \pagestyle{empty} \begin{document} -\fontsize{%f}{%f}%s +%% The empty hbox ensures that a page is printed even for empty inputs. +\fontsize{%f}{%f}\hbox{}%s \end{document} """ % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), encoding='utf-8') @@ -239,7 +241,6 @@ def make_tex_preview(self, tex, fontsize): r""" %s \usepackage[active,showbox,tightpage]{preview} -\usepackage[papersize={72in,72in},body={70in,70in},margin={1in,1in}]{geometry} %% we override the default showbox as it is treated as an error and makes %% the exit status not zero @@ -350,9 +351,16 @@ def make_png(self, tex, fontsize, dpi): # see get_rgba for a discussion of the background if not os.path.exists(pngfile): dvifile = self.make_dvi(tex, fontsize) - self._run_checked_subprocess( - ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", pngfile, dvifile], tex) + cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), + "-T", "tight", "-o", pngfile, dvifile] + # When testing, disable FreeType rendering for reproducibility; but + # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 + # mode, so for it we keep FreeType enabled; the image will be + # slightly off. + if (getattr(mpl, "_called_from_pytest", False) + and mpl._get_executable_info("dvipng").version != "1.16"): + cmd.insert(1, "--freetype0") + self._run_checked_subprocess(cmd, tex) return pngfile def get_grey(self, tex, fontsize=None, dpi=None): @@ -403,7 +411,7 @@ def get_text_width_height_descent(self, tex, fontsize, renderer=None): return width, height + depth, depth else: - # use dviread. It sometimes returns a wrong descent. + # use dviread. dvifile = self.make_dvi(tex, fontsize) with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: page, = dvi diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 77ab7145a7c2..3c37a23b32db 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -23,8 +23,7 @@ @contextlib.contextmanager def _wrap_text(textobj): - """Temporarily inserts newlines to the text if the wrap option is enabled. - """ + """Temporarily inserts newlines if the wrap option is enabled.""" if textobj.get_wrap(): old_text = textobj.get_text() try: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index e46d7d2014b6..0ed9d489c06a 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -168,6 +168,7 @@ import logging import locale import math +from numbers import Integral import numpy as np @@ -584,11 +585,42 @@ def set_powerlimits(self, lims): def format_data_short(self, value): # docstring inherited + if isinstance(value, np.ma.MaskedArray) and value.mask: + return "" + if isinstance(value, Integral): + fmt = "%d" + else: + if self.axis.__name__ in ["xaxis", "yaxis"]: + if self.axis.__name__ == "xaxis": + axis_trf = self.axis.axes.get_xaxis_transform() + axis_inv_trf = axis_trf.inverted() + screen_xy = axis_trf.transform((value, 0)) + neighbor_values = axis_inv_trf.transform( + screen_xy + [[-1, 0], [+1, 0]])[:, 0] + else: # yaxis: + axis_trf = self.axis.axes.get_yaxis_transform() + axis_inv_trf = axis_trf.inverted() + screen_xy = axis_trf.transform((0, value)) + neighbor_values = axis_inv_trf.transform( + screen_xy + [[0, -1], [0, +1]])[:, 1] + delta = abs(neighbor_values - value).max() + else: + # Rough approximation: no more than 1e4 pixels. + delta = self.axis.get_view_interval() / 1e4 + # If e.g. value = 45.67 and delta = 0.02, then we want to round to + # 2 digits after the decimal point (floor(log10(0.02)) = -2); + # 45.67 contributes 2 digits before the decimal point + # (floor(log10(45.67)) + 1 = 2): the total is 4 significant digits. + # A value of 0 contributes 1 "digit" before the decimal point. + sig_digits = max( + 0, + (math.floor(math.log10(abs(value))) + 1 if value else 1) + - math.floor(math.log10(delta))) + fmt = f"%-#.{sig_digits}g" return ( - "" if isinstance(value, np.ma.MaskedArray) and value.mask else self.fix_minus( - locale.format_string("%-12g", (value,)) if self._useLocale else - "%-12g" % value)) + locale.format_string(fmt, (value,)) if self._useLocale else + fmt % value)) def format_data(self, value): # docstring inherited @@ -1267,7 +1299,7 @@ def format_data_short(self, value): class EngFormatter(Formatter): """ - Formats axis values using engineering prefixes to represent powers + Format axis values using engineering prefixes to represent powers of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. """ @@ -1365,7 +1397,7 @@ def __call__(self, x, pos=None): def format_eng(self, num): """ - Formats a number in engineering notation, appending a letter + Format a number in engineering notation, appending a letter representing the power of 1000 of the original number. Some examples: @@ -1449,17 +1481,14 @@ def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False): self._is_latex = is_latex def __call__(self, x, pos=None): - """ - Formats the tick as a percentage with the appropriate scaling. - """ + """Format the tick as a percentage with the appropriate scaling.""" ax_min, ax_max = self.axis.get_view_interval() display_range = abs(ax_max - ax_min) - return self.fix_minus(self.format_pct(x, display_range)) def format_pct(self, x, display_range): """ - Formats the number as a percentage number with the correct + Format the number as a percentage number with the correct number of decimals and adds the percent symbol, if any. If ``self.decimals`` is `None`, the number of digits after the @@ -1532,6 +1561,18 @@ def symbol(self, symbol): self._symbol = symbol +def _if_refresh_overridden_call_and_emit_deprec(locator): + if not locator.refresh.__func__.__module__.startswith("matplotlib."): + cbook.warn_external( + "3.3", message="Automatic calls to Locator.refresh by the draw " + "machinery are deprecated since %(since)s and will be removed in " + "%(removal)s. You are using a third-party locator that overrides " + "the refresh() method; this locator should instead perform any " + "required processing in __call__().") + with cbook._suppress_matplotlib_deprecation_warning(): + locator.refresh() + + class Locator(TickHelper): """ Determine the tick locations; @@ -1654,9 +1695,9 @@ def zoom(self, direction): step = 0.1 * interval * direction self.axis.set_view_interval(vmin + step, vmax - step, ignore=True) + @cbook.deprecated("3.3") def refresh(self): """Refresh internal information based on current limits.""" - pass class IndexLocator(Locator): diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 932352139704..d05ce7ec9135 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -1,5 +1,5 @@ """ -This module is to support the *bbox_inches* parameter in `.Figure.savefig`. +Helper module for the *bbox_inches* parameter in `.Figure.savefig`. """ from matplotlib.transforms import Bbox, TransformedBbox, Affine2D @@ -73,8 +73,8 @@ def restore_bbox(): def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None): """ - This need to be called when figure dpi changes during the drawing - (e.g., rasterizing). It recovers the bbox and re-adjust it with + A function that needs to be called when figure dpi changes during the + drawing (e.g., rasterizing). It recovers the bbox and re-adjust it with the new dpi. """ diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index fb156762be7b..387c23b1b65f 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -1,9 +1,9 @@ """ -This module provides routines to adjust subplot params so that subplots are +Routines to adjust subplot params so that subplots are nicely fit in the figure. In doing so, only axis labels, tick labels, axes titles and offsetboxes that are anchored to axes are currently considered. -Internally, it assumes that the margins (left_margin, etc.) which are +Internally, this module assumes that the margins (left_margin, etc.) which are differences between ax.get_tightbbox and ax.bbox are independent of axes position. This may fail if Axes.adjustable is datalim. Also, This will fail for some cases (for example, left or right margin is affected by xlabel). @@ -230,7 +230,8 @@ def get_renderer(fig): def get_subplotspec_list(axes_list, grid_spec=None): - """Return a list of subplotspec from the given list of axes. + """ + Return a list of subplotspec from the given list of axes. For an instance of axes that does not support subplotspec, None is inserted in the list. diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 2ac69726b005..2a8fc834f3ff 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -1,5 +1,5 @@ """ -matplotlib includes a framework for arbitrary geometric +Matplotlib includes a framework for arbitrary geometric transformations that is used determine the final position of all elements drawn on the canvas. @@ -211,14 +211,16 @@ def frozen(self): class BboxBase(TransformNode): """ - This is the base class of all bounding boxes, and provides read-only access - to its data. A mutable bounding box is provided by the `Bbox` class. + The base class of all bounding boxes. + + This class is immutable; `Bbox` is a mutable subclass. The canonical representation is as two points, with no restrictions on their ordering. Convenience properties are provided to get the left, bottom, right and top edges and width and height, but these are not stored explicitly. """ + is_bbox = True is_affine = True @@ -682,6 +684,82 @@ def intersection(bbox1, bbox2): class Bbox(BboxBase): """ A mutable bounding box. + + Examples + -------- + **Create from known bounds** + + The default constructor takes the boundary "points" ``[[xmin, ymin], + [xmax, ymax]]``. + + >>> Bbox([[1, 1], [3, 7]]) + Bbox([[1.0, 1.0], [3.0, 7.0]]) + + Alternatively, a Bbox can be created from the flattened points array, the + so-called "extents" ``(xmin, ymin, xmax, ymax)`` + + >>> Bbox.from_extents(1, 1, 3, 7) + Bbox([[1.0, 1.0], [3.0, 7.0]]) + + or from the "bounds" ``(xmin, ymin, width, height)``. + + >>> Bbox.from_bounds(1, 1, 2, 6) + Bbox([[1.0, 1.0], [3.0, 7.0]]) + + **Create from collections of points** + + The "empty" object for accumulating Bboxs is the null bbox, which is a + stand-in for the empty set. + + >>> Bbox.null() + Bbox([[inf, inf], [-inf, -inf]]) + + Adding points to the null bbox will give you the bbox of those points. + + >>> box = Bbox.null() + >>> box.update_from_data_xy([[1, 1]]) + >>> box + Bbox([[1.0, 1.0], [1.0, 1.0]]) + >>> box.update_from_data_xy([[2, 3], [3, 2]], ignore=False) + >>> box + Bbox([[1.0, 1.0], [3.0, 3.0]]) + + Setting ``ignore=True`` is equivalent to starting over from a null bbox. + + >>> box.update_from_data_xy([[1, 1]], ignore=True) + >>> box + Bbox([[1.0, 1.0], [1.0, 1.0]]) + + .. warning:: + + It is recommended to always specify ``ignore`` explicitly. If not, the + default value of ``ignore`` can be changed at any time by code with + access to your Bbox, for example using the method `~.Bbox.ignore`. + + **Properties of the ``null`` bbox** + + .. note:: + + The current behavior of `Bbox.null()` may be surprising as it does + not have all of the properties of the "empty set", and as such does + not behave like a "zero" object in the mathematical sense. We may + change that in the future (with a deprecation period). + + The null bbox is the identity for intersections + + >>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.null()) + Bbox([[1.0, 1.0], [3.0, 7.0]]) + + except with itself, where it returns the full space. + + >>> Bbox.intersection(Bbox.null(), Bbox.null()) + Bbox([[-inf, -inf], [inf, inf]]) + + A union containing null will always return the full space (not the other + set!) + + >>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.null()]) + Bbox([[-inf, -inf], [inf, inf]]) """ def __init__(self, points, **kwargs): @@ -690,12 +768,6 @@ def __init__(self, points, **kwargs): ---------- points : ndarray A 2x2 numpy array of the form ``[[x0, y0], [x1, y1]]``. - - Notes - ----- - If you need to create a `Bbox` object from another form - of data, consider the static methods :meth:`unit`, - :meth:`from_bounds` and :meth:`from_extents`. """ BboxBase.__init__(self, **kwargs) points = np.asarray(points, float) @@ -788,7 +860,7 @@ def update_from_path(self, path, ignore=None, updatex=True, updatey=True): - when ``False``, include the existing bounds of the `Bbox`. - when ``None``, use the last value passed to :meth:`ignore`. - updatex, updatey : bool, optional + updatex, updatey : bool, default: True When ``True``, update the x/y values. """ if ignore is None: @@ -825,7 +897,7 @@ def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True): - When ``False``, include the existing bounds of the `Bbox`. - When ``None``, use the last value passed to :meth:`ignore`. - updatex, updatey : bool, optional + updatex, updatey : bool, default: True When ``True``, update the x/y values. """ if len(xy) == 0: @@ -1884,7 +1956,7 @@ def rotate_deg_around(self, x, y, degrees): def translate(self, tx, ty): """ - Adds a translation in place. + Add a translation in place. Returns *self*, so this method can easily be chained with more calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` @@ -1898,7 +1970,7 @@ def translate(self, tx, ty): def scale(self, sx, sy=None): """ - Adds a scale in place. + Add a scale in place. If *sy* is None, the same scale is applied in both the *x*- and *y*-directions. @@ -1917,7 +1989,7 @@ def scale(self, sx, sy=None): def skew(self, xShear, yShear): """ - Adds a skew in place. + Add a skew in place. *xShear* and *yShear* are the shear angles along the *x*- and *y*-axes, respectively, in radians. @@ -1936,7 +2008,7 @@ def skew(self, xShear, yShear): def skew_deg(self, xShear, yShear): """ - Adds a skew in place. + Add a skew in place. *xShear* and *yShear* are the shear angles along the *x*- and *y*-axes, respectively, in degrees. diff --git a/lib/matplotlib/tri/triinterpolate.py b/lib/matplotlib/tri/triinterpolate.py index 6db3db4e547b..b94b84a18a3d 100644 --- a/lib/matplotlib/tri/triinterpolate.py +++ b/lib/matplotlib/tri/triinterpolate.py @@ -14,23 +14,22 @@ class TriInterpolator: """ - Abstract base class for classes used to perform interpolation on - triangular grids. + Abstract base class for classes used to interpolate on a triangular grid. Derived classes implement the following methods: - - ``__call__(x, y)`` , - where x, y are array-like point coordinates of the same shape, and - that returns a masked array of the same shape containing the - interpolated z-values. - - - ``gradient(x, y)`` , - where x, y are array-like point coordinates of the same - shape, and that returns a list of 2 masked arrays of the same shape - containing the 2 derivatives of the interpolator (derivatives of - interpolated z values with respect to x and y). + - ``__call__(x, y)``, + where x, y are array-like point coordinates of the same shape, and + that returns a masked array of the same shape containing the + interpolated z-values. + - ``gradient(x, y)``, + where x, y are array-like point coordinates of the same + shape, and that returns a list of 2 masked arrays of the same shape + containing the 2 derivatives of the interpolator (derivatives of + interpolated z values with respect to x and y). """ + def __init__(self, triangulation, z, trifinder=None): cbook._check_isinstance(Triangulation, triangulation=triangulation) self._triangulation = triangulation @@ -209,7 +208,7 @@ def _interpolate_multikeys(self, x, y, tri_index=None, def _interpolate_single_key(self, return_key, tri_index, x, y): """ - Performs the interpolation at points belonging to the triangulation + Interpolate at points belonging to the triangulation (inside an unmasked triangles). Parameters @@ -232,7 +231,7 @@ def _interpolate_single_key(self, return_key, tri_index, x, y): class LinearTriInterpolator(TriInterpolator): """ - A LinearTriInterpolator performs linear interpolation on a triangular grid. + Linear interpolator on a triangular grid. Each triangle is represented by a plane so that an interpolated value at point (x, y) lies on the plane of the triangle containing (x, y). @@ -287,7 +286,7 @@ def _interpolate_single_key(self, return_key, tri_index, x, y): class CubicTriInterpolator(TriInterpolator): r""" - A CubicTriInterpolator performs cubic interpolation on triangular grids. + Cubic interpolator on a triangular grid. In one-dimension - on a segment - a cubic interpolating function is defined by the values of the function and its derivative at both ends. @@ -450,13 +449,12 @@ def _interpolate_single_key(self, return_key, tri_index, x, y): def _compute_dof(self, kind, dz=None): """ - Computes and returns nodal dofs according to kind + Compute and return nodal dofs according to kind. Parameters ---------- kind : {'min_E', 'geom', 'user'} - Choice of the _DOF_estimator subclass to perform the gradient - estimation. + Choice of the _DOF_estimator subclass to estimate the gradient. dz : tuple of array-likes (dzdx, dzdy), optional Used only if *kind*=user; in this case passed to the :class:`_DOF_estimator_user`. @@ -552,7 +550,7 @@ def _get_jacobian(tris_pts): @staticmethod def _compute_tri_eccentricities(tris_pts): """ - Computes triangle eccentricities + Compute triangle eccentricities. Parameters ---------- @@ -928,11 +926,12 @@ def get_Hrot_from_J(self, J, return_area=False): def get_Kff_and_Ff(self, J, ecc, triangles, Uc): """ - Builds K and F for the following elliptic formulation: + Build K and F for the following elliptic formulation: minimization of curvature energy with value of function at node imposed and derivatives 'free'. - Builds the global Kff matrix in cco format. - Builds the full Ff vec Ff = - Kfc x Uc + + Build the global Kff matrix in cco format. + Build the full Ff vec Ff = - Kfc x Uc. Parameters ---------- @@ -1002,9 +1001,10 @@ def get_Kff_and_Ff(self, J, ecc, triangles, Uc): # element for the TriCubicInterpolator. class _DOF_estimator: """ - Abstract base class for classes used to perform estimation of a function - first derivatives, and deduce the dofs for a CubicTriInterpolator using a + Abstract base class for classes used to estimate a function's first + derivatives, and deduce the dofs for a CubicTriInterpolator using a reduced HCT element formulation. + Derived classes implement ``compute_df(self, **kwargs)``, returning ``np.vstack([dfx, dfy]).T`` where ``dfx, dfy`` are the estimation of the 2 gradient coordinates. @@ -1026,8 +1026,7 @@ def compute_dz(self, **kwargs): def compute_dof_from_df(self): """ - Computes reduced-HCT elements degrees of freedom, knowing the - gradient. + Compute reduced-HCT elements degrees of freedom, from the gradient. """ J = CubicTriInterpolator._get_jacobian(self._tris_pts) tri_z = self.z[self._triangles] @@ -1038,17 +1037,26 @@ def compute_dof_from_df(self): @staticmethod def get_dof_vec(tri_z, tri_dz, J): """ - Computes the dof vector of a triangle, knowing the value of f, df and + Compute the dof vector of a triangle, from the value of f, df and of the local Jacobian at each node. - *tri_z*: array of shape (3,) of f nodal values - *tri_dz*: array of shape (3, 2) of df/dx, df/dy nodal values - *J*: Jacobian matrix in local basis of apex 0 + Parameters + ---------- + tri_z : shape (3,) array + f nodal values. + tri_dz : shape (3, 2) array + df/dx, df/dy nodal values. + J + Jacobian matrix in local basis of apex 0. + + Returns + ------- + dof : shape (9,) array + For each apex ``iapex``:: - Returns dof array of shape (9,) so that for each apex iapex: - dof[iapex*3+0] = f(Ai) - dof[iapex*3+1] = df(Ai).(AiAi+) - dof[iapex*3+2] = df(Ai).(AiAi-)] + dof[iapex*3+0] = f(Ai) + dof[iapex*3+1] = df(Ai).(AiAi+) + dof[iapex*3+2] = df(Ai).(AiAi-) """ npt = tri_z.shape[0] dof = np.zeros([npt, 9], dtype=np.float64) @@ -1116,7 +1124,7 @@ def compute_dz(self): def compute_geom_weights(self): """ - Builds the (nelems x 3) weights coeffs of _triangles angles, + Build the (nelems, 3) weights coeffs of _triangles angles, renormalized so that np.sum(weights, axis=1) == np.ones(nelems) """ weights = np.zeros([np.size(self._triangles, 0), 3]) @@ -1225,12 +1233,11 @@ def compute_dz(self): class _Sparse_Matrix_coo: def __init__(self, vals, rows, cols, shape): """ - Creates a sparse matrix in coo format + Create a sparse matrix in coo format. *vals*: arrays of values of non-null entries of the matrix *rows*: int arrays of rows of non-null entries of the matrix *cols*: int arrays of cols of non-null entries of the matrix *shape*: 2-tuple (n, m) of matrix shape - """ self.n, self.m = shape self.vals = np.asarray(vals, dtype=np.float64) @@ -1271,8 +1278,7 @@ def compress_csr(self): def to_dense(self): """ - Returns a dense matrix representing self. - Mainly for debugging purposes. + Return a dense matrix representing self, mainly for debugging purposes. """ ret = np.zeros([self.n, self.m], dtype=np.float64) nvals = self.vals.size @@ -1285,9 +1291,7 @@ def __str__(self): @property def diag(self): - """ - Returns the (dense) vector of the diagonal elements. - """ + """Return the (dense) vector of the diagonal elements.""" in_diag = (self.rows == self.cols) diag = np.zeros(min(self.n, self.n), dtype=np.float64) # default 0. diag[self.rows[in_diag]] = self.vals[in_diag] @@ -1304,9 +1308,17 @@ def _cg(A, b, x0=None, tol=1.e-10, maxiter=1000): A : _Sparse_Matrix_coo *A* must have been compressed before by compress_csc or compress_csr method. - b : array Right hand side of the linear system. + x0 : array, optional + Starting guess for the solution. Defaults to the zero vector. + tol : float, optional + Tolerance to achieve. The algorithm terminates when the relative + residual is below tol. Default is 1e-10. + maxiter : int, optional + Maximum number of iterations. Iteration will stop after *maxiter* + steps even if the specified tolerance has not been achieved. Defaults + to 1000. Returns ------- @@ -1314,17 +1326,6 @@ def _cg(A, b, x0=None, tol=1.e-10, maxiter=1000): The converged solution. err : float The absolute error np.linalg.norm(A.dot(x) - b) - - Other parameters - ---------------- - x0 : array - Starting guess for the solution. - tol : float - Tolerance to achieve. The algorithm terminates when the relative - residual is below tol. - maxiter : int - Maximum number of iterations. Iteration will stop after *maxiter* - steps even if the specified tolerance has not been achieved. """ n = b.size assert A.n == n @@ -1525,9 +1526,8 @@ def _transpose_vectorized(M): def _roll_vectorized(M, roll_indices, axis): """ - Rolls an array of matrices along an axis according to an array of indices - *roll_indices* - *axis* can be either 0 (rolls rows) or 1 (rolls columns). + Roll an array of matrices along *axis* (0: rows, 1: columns) according to + an array of indices *roll_indices*. """ assert axis in [0, 1] ndim = M.ndim @@ -1554,12 +1554,17 @@ def _roll_vectorized(M, roll_indices, axis): def _to_matrix_vectorized(M): """ - Builds an array of matrices from individuals np.arrays of identical - shapes. - *M*: ncols-list of nrows-lists of shape sh. + Build an array of matrices from individuals np.arrays of identical shapes. - Returns M_res np.array of shape (sh, nrow, ncols) so that: - M_res[..., i, j] = M[i][j] + Parameters + ---------- + M + ncols-list of nrows-lists of shape sh. + + Returns + ------- + M_res : np.array of shape (sh, nrow, ncols) + *M_res* satisfies ``M_res[..., i, j] = M[i][j]``. """ assert isinstance(M, (tuple, list)) assert all(isinstance(item, (tuple, list)) for item in M) @@ -1579,7 +1584,7 @@ def _to_matrix_vectorized(M): def _extract_submatrices(M, block_indices, block_size, axis): """ - Extracts selected blocks of a matrices *M* depending on parameters + Extract selected blocks of a matrices *M* depending on parameters *block_indices* and *block_size*. Returns the array of extracted matrices *Mres* so that :: diff --git a/lib/matplotlib/tri/trirefine.py b/lib/matplotlib/tri/trirefine.py index 542fb006cb61..7c2f6535ee96 100644 --- a/lib/matplotlib/tri/trirefine.py +++ b/lib/matplotlib/tri/trirefine.py @@ -171,10 +171,10 @@ def refine_field(self, z, triinterpolator=None, subdiv=3): @staticmethod def _refine_triangulation_once(triangulation, ancestors=None): """ - This function refines a matplotlib.tri *triangulation* by splitting - each triangle into 4 child-masked_triangles built on the edges midside - nodes. - The masked triangles, if present, are also split but their children + Refine a `.Triangulation` by splitting each triangle into 4 + child-masked_triangles built on the edges midside nodes. + + Masked triangles, if present, are also split, but their children returned masked. If *ancestors* is not provided, returns only a new triangulation: @@ -186,8 +186,8 @@ def _refine_triangulation_once(triangulation, ancestors=None): (child_triangulation, child_ancestors) child_ancestors is defined so that the 4 child masked_triangles share the same index as their father: child_ancestors.shape = (4 * ntri,). - """ + x = triangulation.x y = triangulation.y diff --git a/lib/matplotlib/tri/tritools.py b/lib/matplotlib/tri/tritools.py index 60e9343063bd..c11cb631e9af 100644 --- a/lib/matplotlib/tri/tritools.py +++ b/lib/matplotlib/tri/tritools.py @@ -64,9 +64,8 @@ def circle_ratios(self, rescale=True): Parameters ---------- rescale : bool, default: True - If True, a rescaling will be internally performed (based on - `scale_factors`, so that the (unmasked) triangles fit - exactly inside a unit square mesh. + If True, internally rescale (based on `scale_factors`), so that the + (unmasked) triangles fit exactly inside a unit square mesh. Returns ------- @@ -137,10 +136,10 @@ def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True): Border triangles with incircle/circumcircle radii ratio r/R will be removed if r/R < *min_circle_ratio*. rescale : bool, default: True - If True, a rescaling will first be internally performed (based on - `scale_factors`), so that the (unmasked) triangles fit exactly - inside a unit square mesh. This rescaling accounts for the - difference of scale which might exist between the 2 axis. + If True, first, internally rescale (based on `scale_factors`) so + that the (unmasked) triangles fit exactly inside a unit square + mesh. This rescaling accounts for the difference of scale which + might exist between the 2 axis. Returns ------- diff --git a/lib/matplotlib/type1font.py b/lib/matplotlib/type1font.py index eb69cd1a424b..1c09bdcb1ca3 100644 --- a/lib/matplotlib/type1font.py +++ b/lib/matplotlib/type1font.py @@ -1,10 +1,9 @@ """ -This module contains a class representing a Type 1 font. +A class representing a Type 1 font. This version reads pfa and pfb files and splits them for embedding in pdf files. It also supports SlantFont and ExtendFont transformations, -similarly to pdfTeX and friends. There is no support yet for -subsetting. +similarly to pdfTeX and friends. There is no support yet for subsetting. Usage:: diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py index 034299d38ab5..311764c26be9 100644 --- a/lib/matplotlib/units.py +++ b/lib/matplotlib/units.py @@ -115,16 +115,12 @@ class ConversionInterface: @staticmethod def axisinfo(unit, axis): - """ - Return an `~units.AxisInfo` for the axis with the specified units. - """ + """Return an `.AxisInfo` for the axis with the specified units.""" return None @staticmethod def default_units(x, axis): - """ - Return the default unit for *x* or ``None`` for the given axis. - """ + """Return the default unit for *x* or ``None`` for the given axis.""" return None @staticmethod diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 61a9143a009f..b3f70110482b 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -16,7 +16,7 @@ import numpy as np import matplotlib as mpl -from . import cbook +from . import cbook, ticker from .lines import Line2D from .patches import Circle, Rectangle, Ellipse from .transforms import blended_transform_factory @@ -69,13 +69,11 @@ class Widget: _active = True def set_active(self, active): - """Set whether the widget is active. - """ + """Set whether the widget is active.""" self._active = active def get_active(self): - """Get whether the widget is active. - """ + """Get whether the widget is active.""" return self._active # set_active is overridden by SelectorWidgets. @@ -190,8 +188,6 @@ def __init__(self, ax, label, image=None, self.color = color self.hovercolor = hovercolor - self._lastcolor = color - def _click(self, event): if (self.ignore(event) or event.inaxes != self.ax @@ -214,13 +210,9 @@ def _release(self, event): def _motion(self, event): if self.ignore(event): return - if event.inaxes == self.ax: - c = self.hovercolor - else: - c = self.color - if c != self._lastcolor: + c = self.hovercolor if event.inaxes == self.ax else self.color + if c != self.ax.get_facecolor(): self.ax.set_facecolor(c) - self._lastcolor = c if self.drawon: self.ax.figure.canvas.draw() @@ -256,7 +248,8 @@ class Slider(AxesWidget): val : float Slider value. """ - def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', + + def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, closedmin=True, closedmax=True, slidermin=None, slidermax=None, dragging=True, valstep=None, orientation='horizontal', **kwargs): @@ -278,8 +271,9 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', valinit : float, default: 0.5 The slider initial position. - valfmt : str, default: "%1.2f" - Used to format the slider value, fprint format string. + valfmt : str, default: None + %-format string used to format the slider value. If None, a + `.ScalarFormatter` is used instead. closedmin : bool, default: True Whether the slider interval is closed on the bottom. @@ -347,13 +341,23 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs) self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1) - self.valfmt = valfmt - ax.set_yticks([]) if orientation == 'vertical': ax.set_ylim((valmin, valmax)) + axis = ax.yaxis else: ax.set_xlim((valmin, valmax)) + axis = ax.xaxis + + self.valfmt = valfmt + self._fmt = axis.get_major_formatter() + if not isinstance(self._fmt, ticker.ScalarFormatter): + self._fmt = ticker.ScalarFormatter() + self._fmt.set_axis(axis) + self._fmt.set_useOffset(False) # No additive offset. + self._fmt.set_useMathText(True) # x sign before multiplicative offset. + ax.set_xticks([]) + ax.set_yticks([]) ax.set_navigate(False) self.connect_event('button_press_event', self._update) @@ -365,7 +369,7 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', verticalalignment='bottom', horizontalalignment='center') - self.valtext = ax.text(0.5, -0.02, valfmt % valinit, + self.valtext = ax.text(0.5, -0.02, self._format(valinit), transform=ax.transAxes, verticalalignment='top', horizontalalignment='center') @@ -374,7 +378,7 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', verticalalignment='center', horizontalalignment='right') - self.valtext = ax.text(1.02, 0.5, valfmt % valinit, + self.valtext = ax.text(1.02, 0.5, self._format(valinit), transform=ax.transAxes, verticalalignment='center', horizontalalignment='left') @@ -435,6 +439,15 @@ def _update(self, event): if val not in [None, self.val]: self.set_val(val) + def _format(self, val): + """Pretty-print *val*.""" + if self.valfmt is not None: + return self.valfmt % val + else: + _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax]) + # fmt.get_offset is actually the multiplicative factor, if any. + return s + self._fmt.get_offset() + def set_val(self, val): """ Set slider value to *val* @@ -451,7 +464,7 @@ def set_val(self, val): xy[2] = val, 1 xy[3] = val, 0 self.poly.xy = xy - self.valtext.set_text(self.valfmt % val) + self.valtext.set_text(self._format(val)) if self.drawon: self.ax.figure.canvas.draw_idle() self.val = val @@ -706,50 +719,41 @@ def __init__(self, ax, label, initial='', self.DIST_FROM_LEFT = .05 - self.text = initial - self.label = ax.text(-label_pad, 0.5, label, - verticalalignment='center', - horizontalalignment='right', - transform=ax.transAxes) - self.text_disp = self._make_text_disp(self.text) + self.label = ax.text( + -label_pad, 0.5, label, transform=ax.transAxes, + verticalalignment='center', horizontalalignment='right') + self.text_disp = self.ax.text( + self.DIST_FROM_LEFT, 0.5, initial, transform=self.ax.transAxes, + verticalalignment='center', horizontalalignment='left') self.cnt = 0 self.change_observers = {} self.submit_observers = {} - # If these lines are removed, the cursor won't appear the first - # time the box is clicked: - self.ax.set_xlim(0, 1) - self.ax.set_ylim(0, 1) + ax.set( + xlim=(0, 1), ylim=(0, 1), # s.t. cursor appears from first click. + navigate=False, facecolor=color, + xticks=[], yticks=[]) self.cursor_index = 0 - # Because this is initialized, _render_cursor - # can assume that cursor exists. - self.cursor = self.ax.vlines(0, 0, 0) - self.cursor.set_visible(False) + self.cursor = ax.vlines(0, 0, 0, visible=False, + transform=mpl.transforms.IdentityTransform()) self.connect_event('button_press_event', self._click) self.connect_event('button_release_event', self._release) self.connect_event('motion_notify_event', self._motion) self.connect_event('key_press_event', self._keypress) self.connect_event('resize_event', self._resize) - ax.set_navigate(False) - ax.set_facecolor(color) - ax.set_xticks([]) - ax.set_yticks([]) + self.color = color self.hovercolor = hovercolor - self._lastcolor = color - self.capturekeystrokes = False - def _make_text_disp(self, string): - return self.ax.text(self.DIST_FROM_LEFT, 0.5, string, - verticalalignment='center', - horizontalalignment='left', - transform=self.ax.transAxes) + @property + def text(self): + return self.text_disp.get_text() def _rendercursor(self): # this is a hack to figure out where the cursor should go. @@ -757,25 +761,22 @@ def _rendercursor(self): # and save its dimensions, draw the real text, then put the cursor # at the saved dimensions - widthtext = self.text[:self.cursor_index] - no_text = False - if widthtext in ["", " ", " "]: - no_text = widthtext == "" - widthtext = "," - - wt_disp = self._make_text_disp(widthtext) + # This causes a single extra draw if the figure has never been rendered + # yet, which should be fine as we're going to repeatedly re-render the + # figure later anyways. + if self.ax.figure._cachedRenderer is None: + self.ax.figure.canvas.draw() - self.ax.figure.canvas.draw() - bb = wt_disp.get_window_extent() - inv = self.ax.transData.inverted() - bb = inv.transform(bb) - wt_disp.set_visible(False) - if no_text: - bb[1, 0] = bb[0, 0] - # hack done - self.cursor.set_visible(False) + text = self.text_disp.get_text() # Save value before overwriting it. + widthtext = text[:self.cursor_index] + self.text_disp.set_text(widthtext or ",") + bb = self.text_disp.get_window_extent() + if not widthtext: # Use the comma for the height, but keep width to 0. + bb.x1 = bb.x0 + self.cursor.set( + segments=[[(bb.x1, bb.y0), (bb.x1, bb.y1)]], visible=True) + self.text_disp.set_text(text) - self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1]) self.ax.figure.canvas.draw() def _notify_submit_observers(self): @@ -795,13 +796,13 @@ def _keypress(self, event): return if self.capturekeystrokes: key = event.key - + text = self.text if len(key) == 1: - self.text = (self.text[:self.cursor_index] + key + - self.text[self.cursor_index:]) + text = (text[:self.cursor_index] + key + + text[self.cursor_index:]) self.cursor_index += 1 elif key == "right": - if self.cursor_index != len(self.text): + if self.cursor_index != len(text): self.cursor_index += 1 elif key == "left": if self.cursor_index != 0: @@ -809,19 +810,17 @@ def _keypress(self, event): elif key == "home": self.cursor_index = 0 elif key == "end": - self.cursor_index = len(self.text) + self.cursor_index = len(text) elif key == "backspace": if self.cursor_index != 0: - self.text = (self.text[:self.cursor_index - 1] + - self.text[self.cursor_index:]) + text = (text[:self.cursor_index - 1] + + text[self.cursor_index:]) self.cursor_index -= 1 elif key == "delete": if self.cursor_index != len(self.text): - self.text = (self.text[:self.cursor_index] + - self.text[self.cursor_index + 1:]) - - self.text_disp.remove() - self.text_disp = self._make_text_disp(self.text) + text = (text[:self.cursor_index] + + text[self.cursor_index + 1:]) + self.text_disp.set_text(text) self._rendercursor() self._notify_change_observers() if key == "enter": @@ -831,9 +830,7 @@ def set_val(self, val): newval = str(val) if self.text == newval: return - self.text = newval - self.text_disp.remove() - self.text_disp = self._make_text_disp(self.text) + self.text_disp.set_text(newval) self._rendercursor() self._notify_change_observers() self._notify_submit_observers() @@ -845,37 +842,38 @@ def _notify_change_observers(self): def begin_typing(self, x): self.capturekeystrokes = True - # Check for toolmanager handling the keypress - if self.ax.figure.canvas.manager.key_press_handler_id is not None: - # Disable command keys so that the user can type without - # command keys causing figure to be saved, etc. - self._restore_keymap = ExitStack() + # Disable keypress shortcuts, which may otherwise cause the figure to + # be saved, closed, etc., until the user stops typing. The way to + # achieve this depends on whether toolmanager is in use. + stack = ExitStack() # Register cleanup actions when user stops typing. + self._on_stop_typing = stack.close + toolmanager = getattr( + self.ax.figure.canvas.manager, "toolmanager", None) + if toolmanager is not None: + # If using toolmanager, lock keypresses, and plan to release the + # lock when typing stops. + toolmanager.keypresslock(self) + stack.push(toolmanager.keypresslock.release, self) + else: + # If not using toolmanager, disable all keypress-related rcParams. # Avoid spurious warnings if keymaps are getting deprecated. with cbook._suppress_matplotlib_deprecation_warning(): - self._restore_keymap.enter_context( - mpl.rc_context({k: [] for k in mpl.rcParams - if k.startswith('keymap.')})) - else: - self.ax.figure.canvas.manager.toolmanager.keypresslock(self) + stack.enter_context(mpl.rc_context( + {k: [] for k in mpl.rcParams if k.startswith("keymap.")})) def stop_typing(self): - notifysubmit = False - # Because _notify_submit_users might throw an error in the user's code, - # we only want to call it once we've already done our cleanup. if self.capturekeystrokes: - # Check for toolmanager handling the keypress - if self.ax.figure.canvas.manager.key_press_handler_id is not None: - # since the user is no longer typing, - # reactivate the standard command keys - self._restore_keymap.close() - else: - toolmanager = self.ax.figure.canvas.manager.toolmanager - toolmanager.keypresslock.release(self) + self._on_stop_typing() + self._on_stop_typing = None notifysubmit = True + else: + notifysubmit = False self.capturekeystrokes = False self.cursor.set_visible(False) self.ax.figure.canvas.draw() if notifysubmit: + # Because _notify_submit_observers might throw an error in the + # user's code, only call it once we've already done our cleanup. self._notify_submit_observers() def position_cursor(self, x): @@ -885,23 +883,8 @@ def position_cursor(self, x): self.cursor_index = 0 else: bb = self.text_disp.get_window_extent() - - trans = self.ax.transData - inv = self.ax.transData.inverted() - bb = trans.transform(inv.transform(bb)) - - text_start = bb[0, 0] - text_end = bb[1, 0] - - ratio = (x - text_start) / (text_end - text_start) - - if ratio < 0: - ratio = 0 - if ratio > 1: - ratio = 1 - + ratio = np.clip((x - bb.x0) / bb.width, 0, 1) self.cursor_index = int(len(self.text) * ratio) - self._rendercursor() def _click(self, event): @@ -924,13 +907,9 @@ def _resize(self, event): def _motion(self, event): if self.ignore(event): return - if event.inaxes == self.ax: - c = self.hovercolor - else: - c = self.color - if c != self._lastcolor: + c = self.hovercolor if event.inaxes == self.ax else self.color + if c != self.ax.get_facecolor(): self.ax.set_facecolor(c) - self._lastcolor = c if self.drawon: self.ax.figure.canvas.draw() @@ -1513,9 +1492,7 @@ def ignore(self, event): event.button != self.eventpress.button) def update(self): - """ - Draw using blit() or draw_idle() depending on ``self.useblit``. - """ + """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" if not self.ax.get_visible(): return False if self.useblit: @@ -1529,35 +1506,31 @@ def update(self): return False def _get_data(self, event): - """Get the xdata and ydata for event, with limits""" + """Get the xdata and ydata for event, with limits.""" if event.xdata is None: return None, None - x0, x1 = self.ax.get_xbound() - y0, y1 = self.ax.get_ybound() - xdata = max(x0, event.xdata) - xdata = min(x1, xdata) - ydata = max(y0, event.ydata) - ydata = min(y1, ydata) + xdata = np.clip(event.xdata, *self.ax.get_xbound()) + ydata = np.clip(event.ydata, *self.ax.get_ybound()) return xdata, ydata def _clean_event(self, event): - """Clean up an event + """ + Preprocess an event: - Use prev event if there is no xdata - Limit the xdata and ydata to the axes limits - Set the prev event + - Replace *event* by the previous event if *event* has no ``xdata``. + - Clip ``xdata`` and ``ydata`` to the axes limits. + - Update the previous event. """ if event.xdata is None: event = self._prev_event else: event = copy.copy(event) event.xdata, event.ydata = self._get_data(event) - self._prev_event = event return event def press(self, event): - """Button press handler and validator""" + """Button press handler and validator.""" if not self.ignore(event): event = self._clean_event(event) self.eventpress = event @@ -1572,10 +1545,10 @@ def press(self, event): return False def _press(self, event): - """Button press handler""" + """Button press handler.""" def release(self, event): - """Button release event handler and validator""" + """Button release event handler and validator.""" if not self.ignore(event) and self.eventpress: event = self._clean_event(event) self.eventrelease = event @@ -1587,10 +1560,10 @@ def release(self, event): return False def _release(self, event): - """Button release event handler""" + """Button release event handler.""" def onmove(self, event): - """Cursor move event handler and validator""" + """Cursor move event handler and validator.""" if not self.ignore(event) and self.eventpress: event = self._clean_event(event) self._onmove(event) @@ -1598,18 +1571,18 @@ def onmove(self, event): return False def _onmove(self, event): - """Cursor move event handler""" + """Cursor move event handler.""" def on_scroll(self, event): - """Mouse scroll event handler and validator""" + """Mouse scroll event handler and validator.""" if not self.ignore(event): self._on_scroll(event) def _on_scroll(self, event): - """Mouse scroll event handler""" + """Mouse scroll event handler.""" def on_key_press(self, event): - """Key press event handler and validator for all selection widgets""" + """Key press event handler and validator for all selection widgets.""" if self.active: key = event.key or '' key = key.replace('ctrl', 'control') @@ -1624,8 +1597,7 @@ def on_key_press(self, event): self._on_key_press(event) def _on_key_press(self, event): - """Key press event handler - use for widget-specific key press actions. - """ + """Key press event handler - for widget-specific key press actions.""" def on_key_release(self, event): """Key release event handler and validator.""" @@ -1839,7 +1811,7 @@ def _onmove(self, event): return False def _set_span_xy(self, event): - """Setting the span coordinates""" + """Set the span coordinates.""" x, y = self._get_data(event) if x is None: return @@ -2502,21 +2474,18 @@ class PolygonSelector(_SelectorWidget): The parent axes for the widget. onselect : function When a polygon is completed or modified after completion, - the `onselect` function is called and passed a list of the vertices as + the *onselect* function is called and passed a list of the vertices as ``(xdata, ydata)`` tuples. - useblit : bool, optional - lineprops : dict, optional - The line for the sides of the polygon is drawn with the properties - given by `lineprops`. The default is ``dict(color='k', linestyle='-', - linewidth=2, alpha=0.5)``. - markerprops : dict, optional - The markers for the vertices of the polygon are drawn with the - properties given by `markerprops`. The default is ``dict(marker='o', - markersize=7, mec='k', mfc='k', alpha=0.5)``. - vertex_select_radius : float, optional - A vertex is selected (to complete the polygon or to move a vertex) - if the mouse click is within `vertex_select_radius` pixels of the - vertex. The default radius is 15 pixels. + useblit : bool, default: False + lineprops : dict, default: \ +``dict(color='k', linestyle='-', linewidth=2, alpha=0.5)``. + Artist properties for the line representing the edges of the polygon. + markerprops : dict, default: \ +``dict(marker='o', markersize=7, mec='k', mfc='k', alpha=0.5)``. + Artist properties for the markers drawn at the vertices of the polygon. + vertex_select_radius : float, default: 15px + A vertex is selected (to complete the polygon or to move a vertex) if + the mouse click is within *vertex_select_radius* pixels of the vertex. Examples -------- diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index 4ad80217975a..1850e19eed69 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -42,16 +42,16 @@ def __init__(self, width, height, xdescent, ydescent, 'upper center' : 9, 'center' : 10 - pad : float, optional, default: 0.4 + pad : float, default: 0.4 Padding around the child objects, in fraction of the font size. - borderpad : float, optional, default: 0.5 + borderpad : float, default: 0.5 Border padding, in fraction of the font size. prop : `matplotlib.font_manager.FontProperties`, optional Font property used as a reference for paddings. - frameon : bool, optional, default: True + frameon : bool, default: True If True, draw a box around this artists. **kwargs @@ -113,16 +113,16 @@ def __init__(self, transform, loc, 'upper center' : 9, 'center' : 10 - pad : float, optional, default: 0.4 + pad : float, default: 0.4 Padding around the child objects, in fraction of the font size. - borderpad : float, optional, default: 0.5 + borderpad : float, default: 0.5 Border padding, in fraction of the font size. prop : `matplotlib.font_manager.FontProperties`, optional Font property used as a reference for paddings. - frameon : bool, optional, default: True + frameon : bool, default: True If True, draw a box around this artists. **kwargs @@ -190,10 +190,10 @@ def __init__(self, transform, width, height, angle, loc, Padding around the ellipse, in fraction of the font size. Defaults to 0.1. - borderpad : float, optional, default: 0.1 + borderpad : float, default: 0.1 Border padding, in fraction of the font size. - frameon : bool, optional, default: True + frameon : bool, default: True If True, draw a box around the ellipse. prop : `matplotlib.font_manager.FontProperties`, optional @@ -254,27 +254,27 @@ def __init__(self, transform, size, label, loc, 'upper center' : 9, 'center' : 10 - pad : float, optional, default: 0.1 + pad : float, default: 0.1 Padding around the label and size bar, in fraction of the font size. - borderpad : float, optional, default: 0.1 + borderpad : float, default: 0.1 Border padding, in fraction of the font size. - sep : float, optional, default: 2 + sep : float, default: 2 Separation between the label and the size bar, in points. - frameon : bool, optional, default: True + frameon : bool, default: True If True, draw a box around the horizontal bar and label. - size_vertical : float, optional, default: 0 + size_vertical : float, default: 0 Vertical length of the size bar, given in coordinates of *transform*. - color : str, optional, default: 'black' + color : str, default: 'black' Color for the size bar and label. - label_top : bool, optional, default: False + label_top : bool, default: False If True, the label will be over the size bar. fontproperties : `matplotlib.font_manager.FontProperties`, optional @@ -381,13 +381,13 @@ def __init__(self, transform, label_x, label_y, length=0.15, label_x, label_y : str Label text for the x and y arrows - length : float, optional, default: 0.15 + length : float, default: 0.15 Length of the arrow, given in coordinates of *transform*. - fontsize : float, optional, default: 0.08 + fontsize : float, default: 0.08 Size of label strings, given in coordinates of *transform*. - loc : int, optional, default: 2 + loc : int, default: 2 Location of the direction arrows. Valid location codes are:: 'upper right' : 1, @@ -401,45 +401,45 @@ def __init__(self, transform, label_x, label_y, length=0.15, 'upper center' : 9, 'center' : 10 - angle : float, optional, default: 0 + angle : float, default: 0 The angle of the arrows in degrees. - aspect_ratio : float, optional, default: 1 + aspect_ratio : float, default: 1 The ratio of the length of arrow_x and arrow_y. Negative numbers can be used to change the direction. - pad : float, optional, default: 0.4 + pad : float, default: 0.4 Padding around the labels and arrows, in fraction of the font size. - borderpad : float, optional, default: 0.4 + borderpad : float, default: 0.4 Border padding, in fraction of the font size. - frameon : bool, optional, default: False + frameon : bool, default: False If True, draw a box around the arrows and labels. - color : str, optional, default: 'white' + color : str, default: 'white' Color for the arrows and labels. - alpha : float, optional, default: 1 + alpha : float, default: 1 Alpha values of the arrows and labels - sep_x, sep_y : float, optional, default: 0.01 and 0 respectively + sep_x, sep_y : float, default: 0.01 and 0 respectively Separation between the arrows and labels in coordinates of *transform*. fontproperties : `matplotlib.font_manager.FontProperties`, optional Font properties for the label text. - back_length : float, optional, default: 0.15 + back_length : float, default: 0.15 Fraction of the arrow behind the arrow crossing. - head_width : float, optional, default: 10 + head_width : float, default: 10 Width of arrow head, sent to ArrowStyle. - head_length : float, optional, default: 15 + head_length : float, default: 15 Length of arrow head, sent to ArrowStyle. - tail_width : float, optional, default: 2 + tail_width : float, default: 2 Width of arrow tail, sent to ArrowStyle. text_props, arrow_props : dict diff --git a/lib/mpl_toolkits/axes_grid1/axes_divider.py b/lib/mpl_toolkits/axes_grid1/axes_divider.py index 583a88c73eeb..adfb58199e3f 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_divider.py +++ b/lib/mpl_toolkits/axes_grid1/axes_divider.py @@ -1,13 +1,5 @@ """ -The axes_divider module provides helper classes to adjust the positions of -multiple axes at drawing time. - - Divider: this is the class that is used to calculate the axes - position. It divides the given rectangular area into several sub - rectangles. You initialize the divider by setting the horizontal - and vertical lists of sizes that the division will be based on. You - then use the new_locator method, whose return value is a callable - object that can be used to set the axes_locator of the axes. +Helper classes to adjust the positions of multiple axes at drawing time. """ import numpy as np @@ -21,14 +13,14 @@ class Divider: """ - This class calculates the axes position. It - divides the given rectangular area into several - sub-rectangles. You initialize the divider by setting the - horizontal and vertical lists of sizes - (:mod:`mpl_toolkits.axes_grid1.axes_size`) that the division will - be based on. You then use the new_locator method to create a - callable object that can be used as the axes_locator of the - axes. + An Axes positioning class. + + The divider is initialized with lists of horizontal and vertical sizes + (:mod:`mpl_toolkits.axes_grid1.axes_size`) based on which a given + rectangular area will be divided. + + The `new_locator` method then creates a callable object + that can be used as the *axes_locator* of the axes. """ def __init__(self, fig, pos, horizontal, vertical, @@ -38,17 +30,16 @@ def __init__(self, fig, pos, horizontal, vertical, ---------- fig : Figure pos : tuple of 4 floats - position of the rectangle that will be divided + Position of the rectangle that will be divided. horizontal : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` - sizes for horizontal division + Sizes for horizontal division. vertical : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` - sizes for vertical division + Sizes for vertical division. aspect : bool - if True, the overall rectangular area is reduced - so that the relative part of the horizontal and - vertical scales have the same scale. + Whether overall rectangular area is reduced so that the relative + part of the horizontal and vertical scales have the same scale. anchor : {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'} - placement of the reduced rectangle when *aspect* is True + Placement of the reduced rectangle, when *aspect* is True. """ self._fig = fig diff --git a/lib/mpl_toolkits/axes_grid1/axes_rgb.py b/lib/mpl_toolkits/axes_grid1/axes_rgb.py index 6944e75a4000..6ca6f761fe13 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_rgb.py +++ b/lib/mpl_toolkits/axes_grid1/axes_rgb.py @@ -118,7 +118,8 @@ def __init__(self, *args, pad=0, add_all=True, **kwargs): self._config_axes() def _config_axes(self, line_color='w', marker_edge_color='w'): - """Set the line color and ticks for the axes + """ + Set the line color and ticks for the axes. Parameters ---------- @@ -131,14 +132,14 @@ def _config_axes(self, line_color='w', marker_edge_color='w'): @cbook.deprecated("3.3") def add_RGB_to_figure(self): - """Add the red, green and blue axes to the RGB composite's axes figure - """ + """Add red, green and blue axes to the RGB composite's axes figure.""" self.RGB.get_figure().add_axes(self.R) self.RGB.get_figure().add_axes(self.G) self.RGB.get_figure().add_axes(self.B) def imshow_rgb(self, r, g, b, **kwargs): - """Create the four images {rgb, r, g, b} + """ + Create the four images {rgb, r, g, b}. Parameters ---------- diff --git a/lib/mpl_toolkits/axes_grid1/axes_size.py b/lib/mpl_toolkits/axes_grid1/axes_size.py index cb8944900af5..6b2c3fe1579c 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_size.py +++ b/lib/mpl_toolkits/axes_grid1/axes_size.py @@ -222,7 +222,7 @@ def get_size(self, renderer): def from_any(size, fraction_ref=None): """ - Creates Fixed unit when the first argument is a float, or a + Create a Fixed unit when the first argument is a float, or a Fraction unit if that is a string that ends with %. The second argument is only meaningful when Fraction unit is created. diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 879a3429d846..9b0ee9140950 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -408,7 +408,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', are relative to the parent_axes. Otherwise they are to be understood relative to the bounding box provided via *bbox_to_anchor*. - loc : int or str, optional, default: 1 + loc : int or str, default: 1 Location to place the inset axes. The valid locations are:: 'upper right' : 1, @@ -455,7 +455,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', %(Axes)s - borderpad : float, optional, default: 0.5 + borderpad : float, default: 0.5 Padding between inset axes and the bbox_to_anchor. The units are axes font size, i.e. for a default font size of 10 points *borderpad = 0.5* is equivalent to a padding of 5 points. @@ -527,7 +527,7 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', coordinates (i.e., "zoomed in"), while *zoom* < 1 will shrink the coordinates (i.e., "zoomed out"). - loc : int or str, optional, default: 'upper right' + loc : int or str, default: 'upper right' Location to place the inset axes. The valid locations are:: 'upper right' : 1, @@ -573,7 +573,7 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', %(Axes)s - borderpad : float, optional, default: 0.5 + borderpad : float, default: 0.5 Padding between inset axes and the bbox_to_anchor. The units are axes font size, i.e. for a default font size of 10 points *borderpad = 0.5* is equivalent to a padding of 5 points. diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 226bb04e84b9..84fc9d155b5f 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -537,11 +537,11 @@ class Poly3DCollection(PolyCollection): There is no simple definition of the enclosed surface of a 3D polygon unless the polygon is planar. - In practice, Matplotlib performs the filling on the 2D projection of - the polygon. This gives a correct filling appearance only for planar - polygons. For all other polygons, you'll find orientations in which - the edges of the polygon intersect in the projection. This will lead - to an incorrect visualization of the 3D area. + In practice, Matplotlib fills the 2D projection of the polygon. This + gives a correct filling appearance only for planar polygons. For all + other polygons, you'll find orientations in which the edges of the + polygon intersect in the projection. This will lead to an incorrect + visualization of the 3D area. If you need filled areas, it is recommended to create them via `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a @@ -578,7 +578,7 @@ def __init__(self, verts, *args, zsort='average', **kwargs): def set_zsort(self, zsort): """ - Sets the calculation method for the z-order. + Set the calculation method for the z-order. Parameters ---------- @@ -610,7 +610,7 @@ def set_verts(self, verts, closed=True): self._closed = closed def set_verts_and_codes(self, verts, codes): - """Sets 3D vertices with path codes.""" + """Set 3D vertices with path codes.""" # set vertices with closed=False to prevent PolyCollection from # setting path codes self.set_verts(verts, closed=False) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index c7662f8a399f..cfd645c880a3 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -60,9 +60,9 @@ def __init__( The parent figure. rect : (float, float, float, float) The ``(left, bottom, width, height)`` axes position. - azim : float, optional, default: -60 + azim : float, default: -60 Azimuthal viewing angle. - elev : float, optional, default: 30 + elev : float, default: 30 Elevation viewing angle. sharez : Axes3D, optional Other axes to share z-limits with. @@ -839,10 +839,7 @@ def set_zscale(self, value, **kwargs): """) def clabel(self, *args, **kwargs): - """ - This function is currently not implemented for 3D axes. - Returns *None*. - """ + """Currently not implemented for 3D axes, and returns *None*.""" return None def view_init(self, elev=None, azim=None): @@ -1061,10 +1058,11 @@ def format_coord(self, xd, yd): return 'x=%s, y=%s, z=%s' % (xs, ys, zs) def _on_move(self, event): - """Mouse moving + """ + Mouse moving. - button-1 rotates by default. Can be set explicitly in mouse_init(). - button-3 zooms by default. Can be set explicitly in mouse_init(). + By default, button-1 rotates and button-3 zooms; these buttons can be + modified via `mouse_init`. """ if not self.button_pressed: @@ -1532,7 +1530,7 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, def _generate_normals(self, polygons): """ - Takes a list of polygons and return an array of their normals. + Compute the normals of a list of polygons. Normals point towards the viewer for a face with its vertices in counterclockwise order, following the right hand rule. @@ -1766,7 +1764,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, A colormap for the surface patches. norm : Normalize An instance of Normalize to map values to colors. - vmin, vmax : scalar, optional, default: None + vmin, vmax : scalar, default: None Minimum and maximum value to map. shade : bool, default: True Whether to shade the facecolors. Shading is always disabled when @@ -2121,17 +2119,17 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, ---------- xs, ys : array-like The data positions. - zs : float or array-like, optional, default: 0 + zs : float or array-like, default: 0 The z-positions. Either an array of the same length as *xs* and *ys* or a single value to place all points in the same plane. - zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, optional, default: 'z' + zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z' The axis direction for the *zs*. This is useful when plotting 2D data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting *zdir* to 'y' then plots the data to the x-z-plane. See also :doc:`/gallery/mplot3d/2dcollections3d`. - s : scalar or array-like, optional, default: 20 + s : scalar or array-like, default: 20 The marker size in points**2. Either an array of the same length as *xs* and *ys* or a single value to make all markers the same size. @@ -2145,7 +2143,7 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, - A 2-D array in which the rows are RGB or RGBA. For more details see the *c* argument of `~.axes.Axes.scatter`. - depthshade : bool, optional, default: True + depthshade : bool, default: True Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. @@ -2236,7 +2234,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): def bar3d(self, x, y, z, dx, dy, dz, color=None, zsort='average', shade=True, lightsource=None, *args, **kwargs): - """Generate a 3D barplot. + """ + Generate a 3D barplot. This method creates three dimensional barplot where the width, depth, height, and color of the bars can all be uniquely set. @@ -2274,7 +2273,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, zsort : str, optional The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection` - shade : bool, optional, default: True + shade : bool, default: True When true, this shades the dark sides of the bars (relative to the plot's source of light). @@ -2685,7 +2684,7 @@ def _broadcast_color_arg(color, name): voxel_faces = defaultdict(list) def permutation_matrices(n): - """Generator of cyclic permutation matrices.""" + """Generate cyclic permutation matrices.""" mat = np.eye(n, dtype=np.intp) for i in range(n): yield mat diff --git a/matplotlibrc.template b/matplotlibrc.template index 2596a67f309b..04b87c9a932b 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -92,7 +92,7 @@ #backend_fallback: True #interactive: False -#toolbar: toolbar2 # {None, toolbar2} +#toolbar: toolbar2 # {None, toolbar2, toolmanager} #timezone: UTC # a pytz timezone string, e.g., US/Central or Europe/Paris @@ -326,7 +326,7 @@ #mathtext.sf: sans #mathtext.tt: monospace #mathtext.fallback: cm # Select fallback font from ['cm' (Computer Modern), 'stix' - # 'stixsans'] when a symbol can not be found in one of the + # 'stixsans'] when a symbol can not be found in one of the # custom math fonts. Select 'None' to not perform fallback # and replace the missing character by a dummy symbol. #mathtext.default: it # The default font to use for math. @@ -352,6 +352,7 @@ #axes.titleweight: normal # font weight of title #axes.titlecolor: auto # color of the axes title, auto falls back to # text.color as default value +#axes.titley: None # position title (axes relative units). None implies auto #axes.titlepad: 6.0 # pad between axes and title in points #axes.labelsize: medium # fontsize of the x any y labels #axes.labelpad: 4.0 # space between label and axis diff --git a/setup.py b/setup.py index 2548d4f82d66..d4daedd739f3 100644 --- a/setup.py +++ b/setup.py @@ -13,11 +13,13 @@ if sys.version_info < min_version: error = """ Beginning with Matplotlib 3.1, Python {0} or above is required. +You are using Python {1}. This may be due to an out of date pip. Make sure you have pip >= 9.0.1. -""".format('.'.join(str(n) for n in min_version)), +""".format('.'.join(str(n) for n in min_version), + '.'.join(str(n) for n in sys.version_info[:3])) sys.exit(error) from pathlib import Path @@ -112,8 +114,9 @@ def _download_jquery_to(dest): try: buff = download_or_cache(url, sha) except Exception: - raise IOError(f"Failed to download jquery-ui. Please download " - f"{url} and extract it to {dest}.") + raise IOError( + "Failed to download jquery-ui. Please download " + "{url} and extract it to {dest}.".format(url=url, dest=dest)) with ZipFile(buff) as zf: zf.extractall(dest) @@ -154,7 +157,7 @@ def run(self): # If the user just queries for information, don't bother figuring out which # packages to build or install. if not (any('--' + opt in sys.argv - for opt in [*Distribution.display_option_names, 'help']) + for opt in Distribution.display_option_names + ['help']) or 'clean' in sys.argv): # Go through all of the packages and figure out which ones we are # going to build/install. @@ -169,10 +172,11 @@ def run(self): try: message = package.check() except setupext.Skipped as e: - print_status(package.name, f"no [{e}]") + print_status(package.name, "no [{e}]".format(e=e)) continue if message is not None: - print_status(package.name, f"yes [{message}]") + print_status(package.name, + "yes [{message}]".format(message=message)) good_packages.append(package) print_raw() diff --git a/tutorials/intermediate/color_cycle.py b/tutorials/intermediate/color_cycle.py index 1cb8a046dc5c..16c713f85d22 100644 --- a/tutorials/intermediate/color_cycle.py +++ b/tutorials/intermediate/color_cycle.py @@ -9,7 +9,7 @@ .. note:: More complete documentation of the ``cycler`` API can be found - `here `_. + `here `_. This example demonstrates two different APIs: diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 1975153db181..aa6398846843 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -30,12 +30,11 @@ .. warning:: - Currently Constrained Layout is **experimental**. The - behaviour and API are subject to change, or the whole functionality - may be removed without a deprecation period. If you *require* your - plots to be absolutely reproducible, get the Axes positions after - running Constrained Layout and use ``ax.set_position()`` in your code - with ``constrained_layout=False``. + Constrained layout can lead to irreproducible plots + as the solver sometimes returns slightly different results. + If you *require* your plots to be absolutely reproducible, get the + Axes positions after running Constrained Layout and use + ``ax.set_position()`` in your code with ``constrained_layout=False``. Simple Example ============== diff --git a/tutorials/intermediate/gridspec.py b/tutorials/intermediate/gridspec.py index e1b007a10020..8da66d3eed7a 100644 --- a/tutorials/intermediate/gridspec.py +++ b/tutorials/intermediate/gridspec.py @@ -226,7 +226,6 @@ # spines in each of the inner 3x3 grids. import numpy as np -from itertools import product def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): @@ -234,34 +233,24 @@ def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): fig11 = plt.figure(figsize=(8, 8), constrained_layout=False) - -# gridspec inside gridspec -outer_grid = fig11.add_gridspec(4, 4, wspace=0.0, hspace=0.0) - -for i in range(16): - inner_grid = outer_grid[i].subgridspec(3, 3, wspace=0.0, hspace=0.0) - a, b = int(i/4)+1, i % 4+1 - for j, (c, d) in enumerate(product(range(1, 4), repeat=2)): - ax = fig11.add_subplot(inner_grid[j]) - ax.plot(*squiggle_xy(a, b, c, d)) - ax.set_xticks([]) - ax.set_yticks([]) - fig11.add_subplot(ax) - -all_axes = fig11.get_axes() +outer_grid = fig11.add_gridspec(4, 4, wspace=0, hspace=0) + +for a in range(4): + for b in range(4): + # gridspec inside gridspec + inner_grid = outer_grid[a, b].subgridspec(3, 3, wspace=0, hspace=0) + for c in range(3): + for d in range(3): + ax = fig11.add_subplot(inner_grid[c, d]) + ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1)) + ax.set(xticks=[], yticks=[]) # show only the outside spines -for ax in all_axes: - for sp in ax.spines.values(): - sp.set_visible(False) - if ax.is_first_row(): - ax.spines['top'].set_visible(True) - if ax.is_last_row(): - ax.spines['bottom'].set_visible(True) - if ax.is_first_col(): - ax.spines['left'].set_visible(True) - if ax.is_last_col(): - ax.spines['right'].set_visible(True) +for ax in fig11.get_axes(): + ax.spines['top'].set_visible(ax.is_first_row()) + ax.spines['bottom'].set_visible(ax.is_last_row()) + ax.spines['left'].set_visible(ax.is_first_col()) + ax.spines['right'].set_visible(ax.is_last_col()) plt.show() diff --git a/tutorials/intermediate/tight_layout_guide.py b/tutorials/intermediate/tight_layout_guide.py index b1b047a3e7d3..befd70d48d6a 100644 --- a/tutorials/intermediate/tight_layout_guide.py +++ b/tutorials/intermediate/tight_layout_guide.py @@ -6,9 +6,9 @@ How to use tight-layout to fit plots within your figure cleanly. *tight_layout* automatically adjusts subplot params so that the -subplot(s) fits in to the figure area. This is an experimental -feature and may not work for some cases. It only checks the extents -of ticklabels, axis labels, and titles. +subplot(s) fits in to the figure area. This feature may not work +for some cases. It only checks the extents of ticklabels, axis labels, +and titles. An alternative to *tight_layout* is :doc:`constrained_layout `. diff --git a/tutorials/introductory/customizing.py b/tutorials/introductory/customizing.py index 11f7c5c2dcc0..97baedda6427 100644 --- a/tutorials/introductory/customizing.py +++ b/tutorials/introductory/customizing.py @@ -121,8 +121,8 @@ plt.plot(data) ############################################################################### -# Note, that in order to change the usual `plot` color you have to change the -# *prop_cycle* property of *axes*: +# Note, that in order to change the usual `~.Axes.plot` color you have to +# change the *prop_cycle* property of *axes*: mpl.rcParams['axes.prop_cycle'] = cycler(color=['r', 'g', 'b', 'y']) plt.plot(data) # first color is red diff --git a/tutorials/introductory/lifecycle.py b/tutorials/introductory/lifecycle.py index 7b75e4239065..55270a667728 100644 --- a/tutorials/introductory/lifecycle.py +++ b/tutorials/introductory/lifecycle.py @@ -12,7 +12,7 @@ .. note:: - This tutorial is based off of + This tutorial is based on `this excellent blog post `_ by Chris Moffitt. It was transformed into this tutorial by Chris Holdgraf. diff --git a/tutorials/text/text_intro.py b/tutorials/text/text_intro.py index 8eab1136d72a..e5592336d0d8 100644 --- a/tutorials/text/text_intro.py +++ b/tutorials/text/text_intro.py @@ -354,8 +354,7 @@ def formatoddticks(x, pos): - """Format odd tick positions - """ + """Format odd tick positions.""" if x % 2: return '%1.2f' % x else: