diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 9ee39d0c7841..b567c015dc2c 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -28,21 +28,31 @@ jobs: - name: Install cibuildwheel run: | - python -m pip install cibuildwheel==1.5.5 + python -m pip install cibuildwheel==1.6.3 - name: Copy setup.cfg to configure wheel run: | cp setup.cfg.template setup.cfg + - name: Build wheels for CPython 3.9 + run: | + python -m cibuildwheel --output-dir dist + env: + CIBW_BUILD: "cp39-*" + CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 + CIBW_MANYLINUX_I686_IMAGE: manylinux1 + CIBW_BEFORE_BUILD: pip install certifi numpy==1.19.3 + MPL_DISABLE_FH4: "yes" + - name: Build wheels for CPython run: | python -m cibuildwheel --output-dir dist env: CIBW_BUILD: "cp3?-*" - CIBW_SKIP: "cp35-* cp36-*" + CIBW_SKIP: "cp35-* cp36-* cp39-*" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_MANYLINUX_I686_IMAGE: manylinux1 - CIBW_BEFORE_BUILD: pip install numpy==1.15 + CIBW_BEFORE_BUILD: pip install certifi numpy==1.15 MPL_DISABLE_FH4: "yes" - name: Build wheels for CPython 3.6 @@ -52,7 +62,7 @@ jobs: CIBW_BUILD: "cp36-*" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_MANYLINUX_I686_IMAGE: manylinux1 - CIBW_BEFORE_BUILD: pip install numpy==1.15 + CIBW_BEFORE_BUILD: pip install certifi numpy==1.15 MPL_DISABLE_FH4: "yes" if: > startsWith(github.ref, 'refs/heads/v3.3') || @@ -63,7 +73,7 @@ jobs: python -m cibuildwheel --output-dir dist env: CIBW_BUILD: "pp3?-*" - CIBW_BEFORE_BUILD: pip install numpy==1.15 + CIBW_BEFORE_BUILD: pip install certifi numpy==1.15 if: > runner.os != 'Windows' && ( startsWith(github.ref, 'refs/heads/v3.3') || diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index ba142edd6aed..9ec0abf942ba 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -18,11 +18,11 @@ jobs: - name: Set up reviewdog run: | - mkdir -p $HOME/bin + mkdir -p "$HOME/bin" curl -sfL \ https://github.com/reviewdog/reviewdog/raw/master/install.sh | \ - sh -s -- -b $HOME/bin - echo ::add-path::$HOME/bin + sh -s -- -b "$HOME/bin" + echo "$HOME/bin" >> $GITHUB_PATH - name: Run flake8 env: diff --git a/.travis.yml b/.travis.yml index d3b9780d8105..59e6f5b3fb5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,6 +87,7 @@ matrix: env: - PRE=--pre - os: osx + osx_image: xcode9 language: generic # https://github.com/travis-ci/travis-ci/issues/2312 only: master cache: @@ -101,26 +102,15 @@ matrix: allow_failures: - python: "nightly" -before_install: | +before_install: +- | + # Install OS dependencies and set up ccache case "$TRAVIS_OS_NAME" in linux) export PATH=/usr/lib/ccache:$PATH ;; osx) - set -e - ci/silence brew update - brew uninstall numpy gdal postgis - brew unlink python@2 - brew install python || brew upgrade python - brew install ffmpeg imagemagick mplayer ccache - hash -r - which python - python --version - set +e - # We could install ghostscript and inkscape here to test svg and pdf - # but this makes the test time really long. - # brew install ghostscript inkscape - export PATH=/usr/local/opt/python/libexec/bin:/usr/local/opt/ccache/libexec:$PATH + ci/osx-deps ;; esac @@ -143,19 +133,28 @@ install: # install was successful by trying to import the toolkit (sometimes, the # install appears to be successful but shared libraries cannot be loaded at # runtime, so an actual import is a better check). - python -mpip install --upgrade pycairo cairocffi>=0.8 - python -mpip install --upgrade PyGObject && - python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && - echo 'PyGObject is available' || - echo 'PyGObject is not available' - python -mpip install --upgrade pyqt5 && - python -c 'import PyQt5.QtCore' && - echo 'PyQt5 is available' || - echo 'PyQt5 is not available' - python -mpip install --upgrade pyside2 && - python -c 'import PySide2.QtCore' && - echo 'PySide2 is available' || - echo 'PySide2 is not available' + + # PyGObject, pycairo, and cariocffi do not install on OSX 10.12 + + # There are not functioning wheels available for OSX 10.12 (as of + # Sept 2020) for either pyqt5 (there are only wheels for 10.13+) + # or pyside2 (the latest version (5.13.2) with 10.12 wheels has a + # fatal to us bug, it was fixed in 5.14.0 which has 10.13 wheels) + if [[ $TRAVIS_OS_NAME != 'osx' ]]; then + python -mpip install --upgrade pycairo cairocffi>=0.8 + python -mpip install --upgrade PyGObject && + python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && + echo 'PyGObject is available' || + echo 'PyGObject is not available' + python -mpip install --upgrade pyqt5 && + python -c 'import PyQt5.QtCore' && + echo 'PyQt5 is available' || + echo 'PyQt5 is not available' + python -mpip install --upgrade pyside2 && + python -c 'import PySide2.QtCore' && + echo 'PySide2 is available' || + echo 'PySide2 is not available' + fi python -mpip install --upgrade \ -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04 \ wxPython && @@ -169,7 +168,7 @@ install: export CPPFLAGS=--coverage fi - | - python -mpip install -ve . # Install Matplotlib. + python -mpip install -e . # Install Matplotlib. - | if [[ $TRAVIS_OS_NAME != 'osx' ]]; then unset CPPFLAGS diff --git a/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER b/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER new file mode 100644 index 000000000000..0bc1fa7060b7 --- /dev/null +++ b/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER @@ -0,0 +1,108 @@ +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. diff --git a/ci/osx-deps b/ci/osx-deps new file mode 100755 index 000000000000..2f00bb9ffa67 --- /dev/null +++ b/ci/osx-deps @@ -0,0 +1,65 @@ +#!/bin/bash + +set -euo pipefail +cache="$HOME"/.cache/matplotlib + +fold_start() { + key=$1 + title=$2 + echo -e "travis_fold:start:$key\e[2K" + echo -e "travis_time:start:$key\e[2K" + tick="$(date +%s)" + echo "$title" +} + +fold_end() { + key=$1 + tock="$(date +%s)" + nano=000000000 + echo -e "travis_time:end:$key:start=$tick$nano,finish=$tock$nano,duration=$((tock - tick))$nano\e[2K" + echo -e "travis_fold:end:$key\e[2K" +} + +cached_download() { + file=$1 + url=$2 + shasum=$3 + path="$cache/$file" + if [[ ! -f "$path" + || "$(shasum -a 256 "$path" | awk '{print $1}')" != "$shasum" ]] + then + curl -L -o "$path" "$url" + fi +} + +fold_start Python "Install Python 3.8 from python.org" +cached_download python-3.8.5-macosx10.9.pkg \ + https://www.python.org/ftp/python/3.8.5/python-3.8.5-macosx10.9.pkg \ + e27c5a510c10f830084fb9c60b9e9aa8719d92e4537a80e6b4252c02396f0d29 +sudo installer -package "$cache"/python-3.8.5-macosx10.9.pkg -target / +sudo ln -s /usr/local/bin/python3 /usr/local/bin/python +hash -r +fold_end Python + +fold_start ccache 'Install ccache (compile it ourselves)' +cached_download ccache-3.7.11.tar.xz \ + https://github.com/ccache/ccache/releases/download/v3.7.11/ccache-3.7.11.tar.xz \ + 8d450208099a4d202bd7df87caaec81baee20ce9dd62da91e9ea7b95a9072f68 +tar xf "$cache"/ccache-3.7.11.tar.xz +pushd ccache-3.7.11 +./configure --prefix=/usr/local +make -j2 +make install +popd +for compiler in clang clang++ cc gcc c++ g++; do + ln -sf ccache /usr/local/bin/$compiler +done +fold_end ccache + +fold_start freetype 'Install freetype (just unpack into the build directory)' +cached_download freetype-2.6.1.tar.gz \ + https://download.savannah.gnu.org/releases/freetype/freetype-2.6.1.tar.gz \ + 0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 +mkdir -p build +tar -x -C build -f "$cache"/freetype-2.6.1.tar.gz +fold_end freetype diff --git a/doc/_static/zenodo_cache/4030140.svg b/doc/_static/zenodo_cache/4030140.svg new file mode 100644 index 000000000000..8fcb71dead83 --- /dev/null +++ b/doc/_static/zenodo_cache/4030140.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.4030140 + + + 10.5281/zenodo.4030140 + + + \ No newline at end of file diff --git a/doc/api/prev_api_changes/api_changes_3.1.0.rst b/doc/api/prev_api_changes/api_changes_3.1.0.rst index 2c0f629729db..b6e3ff8c4733 100644 --- a/doc/api/prev_api_changes/api_changes_3.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.1.0.rst @@ -727,7 +727,7 @@ Mathtext changes Deprecations ~~~~~~~~~~~~ -- The ``\stackrel`` mathtext command hsa been deprecated (it behaved differently +- The ``\stackrel`` mathtext command has been deprecated (it behaved differently from LaTeX's ``\stackrel``. To stack two mathtext expressions, use ``\genfrac{left-delim}{right-delim}{fraction-bar-thickness}{}{top}{bottom}``. - The ``\mathcircled`` mathtext command (which is not a real TeX command) diff --git a/doc/citing.rst b/doc/citing.rst index cea1c2b96319..cdc4c60fcbd0 100644 --- a/doc/citing.rst +++ b/doc/citing.rst @@ -39,6 +39,9 @@ By version .. START OF AUTOGENERATED +v3.3.2 + .. image:: _static/zenodo_cache/4030140.svg + :target: https://doi.org/10.5281/zenodo.4030140 v3.3.1 .. image:: _static/zenodo_cache/3984190.svg :target: https://doi.org/10.5281/zenodo.3984190 diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index 7ef8dd1111b6..ab8f8384b8aa 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -3,91 +3,94 @@ GitHub Stats ============ -GitHub stats for 2020/08/14 - 2020/09/15 (tag: v3.3.1) +GitHub stats for 2020/09/15 - 2020/11/11 (tag: v3.3.2) These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 15 issues and merged 39 pull requests. -The full list can be seen `on GitHub `__ +We closed 14 issues and merged 46 pull requests. +The full list can be seen `on GitHub `__ -The following 14 authors contributed 61 commits. +The following 11 authors contributed 73 commits. * Antony Lee -* Bruno Beltran * David Stansby -* David Young * Elliott Sales de Andrade -* Greg Lucas +* Eric Larson * Jody Klymak -* johnthagen * Jouni K. Seppänen -* Richard Sheridan -* richardsheridan * Ryan May +* shevawen +* Stephen Sinclair * Thomas A Caswell * Tim Hoffmann GitHub issues and pull requests: -Pull Requests (39): - -* :ghpull:`18488`: Backport PR #18483 on branch v3.3.x (DOC: reword non-monotonic cell center warning) -* :ghpull:`18483`: DOC: reword non-monotonic cell center warning -* :ghpull:`18485`: Backport PR #18475 on branch v3.3.x (BF: ensure exception caught if no kpeswitch) -* :ghpull:`18482`: Backport PR #18398 on branch v3.3.x (Warn on non-increasing/decreasing pcolor coords) -* :ghpull:`18484`: Backport PR #18458: Fix huge imshow range -* :ghpull:`18475`: BF: ensure exception caught if no kpeswitch -* :ghpull:`18458`: Fix huge imshow range -* :ghpull:`18398`: Warn on non-increasing/decreasing pcolor coords -* :ghpull:`18479`: Nbagg backports -* :ghpull:`18454`: nbagg: Use OutputArea event to trigger figure close. -* :ghpull:`18469`: Backport PR #18464 on branch v3.3.x (Remove extra stickies in barstacked histogram.) -* :ghpull:`18464`: Remove extra stickies in barstacked histogram. -* :ghpull:`18459`: Backport PR #18393 on branch v3.3.x (Fix Axis scale on twinned Axes.) -* :ghpull:`18393`: Fix Axis scale on twinned Axes. -* :ghpull:`18441`: Backport PR #18395: TkAgg bugfix: deselect buttons that are not the current _Mode -* :ghpull:`18395`: TkAgg bugfix: deselect buttons that are not the current _Mode -* :ghpull:`18380`: Backport PR #18374 on branch v3.3.x (FIX: make _reshape_2D accept pandas df with string indices) -* :ghpull:`18374`: FIX: make _reshape_2D accept pandas df with string indices -* :ghpull:`18376`: Backport PR #18298 on branch v3.3.x (Include license files in built distribution) -* :ghpull:`18375`: Backport PR #18293 on branch v3.3.x (Fix scatter3d color/linewidth re-projection) -* :ghpull:`18298`: Include license files in built distribution -* :ghpull:`18293`: Fix scatter3d color/linewidth re-projection -* :ghpull:`18361`: nbagg: Store DPI ratio on figure instead of window. -* :ghpull:`18354`: Backport PR #18352 on branch v3.3.x (Avoid triggering backend resolution during qt initial import.) -* :ghpull:`18352`: Avoid triggering backend resolution during qt initial import. -* :ghpull:`18335`: Backport PR #18322 on branch v3.3.x (Disable FH4 so that we don't require VCRUNTIME140_1.dll.) -* :ghpull:`18322`: Disable FH4 so that we don't require VCRUNTIME140_1.dll. -* :ghpull:`18333`: Backport PR #18328 on branch v3.3.x (Add missing check for None in Qt toolmanager.) -* :ghpull:`18328`: Add missing check for None in Qt toolmanager. -* :ghpull:`18309`: Backport PR #18304 on branch v3.3.x (Fix canvas redraws during motion in figures with a Button or TextBox) -* :ghpull:`18304`: Fix canvas redraws during motion in figures with a Button or TextBox -* :ghpull:`18297`: Backport PR #18288 on branch v3.3.x (FIX: check if axes is off page before repositioning title) -* :ghpull:`18288`: FIX: check if axes is off page before repositioning title -* :ghpull:`18269`: Backport PR #18266 on branch v3.3.x (Fix Path.get_extents for empty paths.) -* :ghpull:`18266`: Fix Path.get_extents for empty paths. -* :ghpull:`18263`: Backport PR #18260 on branch v3.3.x (Add parent widget to IntVar) -* :ghpull:`18260`: Add parent widget to IntVar -* :ghpull:`18253`: Backport PR #18245 on branch v3.3.x -* :ghpull:`18245`: MNT: do a better job guessing the GUI framework in use - -Issues (15): - -* :ghissue:`18415`: imshow with LogNorm crashes with certain inputs -* :ghissue:`18447`: nbagg: Closing a figure from the notebook does not close the python figure -* :ghissue:`18470`: interactive plots slow with matplotlib 3.3.1 -* :ghissue:`18457`: Incorrect log y-scale for histogram with partitioned and barstacked data -* :ghissue:`18385`: twinx not respecting log-scale -* :ghissue:`18371`: Plotting a pandas DataFrame with string MultiIndex -* :ghissue:`18296`: LICENSE file(s) not included in published PyPI package -* :ghissue:`18287`: scatter3D assigns wrong color to points for some plot orientations -* :ghissue:`18292`: ImportError: DLL load failed with Matplotlib 3.3.1 on Windows -* :ghissue:`18327`: Tool Manager: adding buttons to toolbar fails with matplotlib version 3.3.1 using Qt backend -* :ghissue:`18324`: Poor UI responsiveness of 3.3.1 compared with 3.2.2 for interactive mode UI using widgets -* :ghissue:`18303`: Canvas redraws during any motion when Button is present -* :ghissue:`18283`: Automatic title placement wrong if parent axes is off the page -* :ghissue:`18254`: scatter(..., marker='') raises on drawing with mpl3.3.1 -* :ghissue:`18259`: New IntVar needs a parent widget +Pull Requests (46): + +* :ghpull:`18936`: Backport PR #18929 on branch v3.3.x +* :ghpull:`18929`: FIX: make sure scalarmappable updates are handled correctly in 3D +* :ghpull:`18928`: Backport PR #18842 on branch v3.3.x (Add CPython 3.9 wheels.) +* :ghpull:`18842`: Add CPython 3.9 wheels. +* :ghpull:`18921`: Backport PR #18732 on branch v3.3.x (Add a ponyfill for ResizeObserver on older browsers.) +* :ghpull:`18732`: Add a ponyfill for ResizeObserver on older browsers. +* :ghpull:`18886`: Backport #18860 on branch v3.3.x +* :ghpull:`18860`: FIX: stop deprecation message colorbar +* :ghpull:`18845`: Backport PR #18839 on branch v3.3.x +* :ghpull:`18843`: Backport PR #18756 on branch v3.3.x (FIX: improve date performance regression) +* :ghpull:`18850`: Backport CI fixes to v3.3.x +* :ghpull:`18839`: MNT: make sure we do not mutate input in Text.update +* :ghpull:`18838`: Fix ax.set_xticklabels(fontproperties=fp) +* :ghpull:`18756`: FIX: improve date performance regression +* :ghpull:`18787`: Backport PR #18769 on branch v3.3.x +* :ghpull:`18786`: Backport PR #18754 on branch v3.3.x (FIX: make sure we have more than 1 tick with small log ranges) +* :ghpull:`18754`: FIX: make sure we have more than 1 tick with small log ranges +* :ghpull:`18769`: Support ``ax.grid(visible=)``. +* :ghpull:`18778`: Backport PR #18773 on branch v3.3.x (Update to latest cibuildwheel release.) +* :ghpull:`18773`: Update to latest cibuildwheel release. +* :ghpull:`18755`: Backport PR #18734 on branch v3.3.x (Fix deprecation warning in GitHub Actions.) +* :ghpull:`18734`: Fix deprecation warning in GitHub Actions. +* :ghpull:`18725`: Backport PR #18533 on branch v3.3.x +* :ghpull:`18723`: Backport PR #18584 on branch v3.3.x (Fix setting 0-timeout timer with Tornado.) +* :ghpull:`18676`: Backport PR #18670 on branch v3.3.x (MNT: make certifi actually optional) +* :ghpull:`18670`: MNT: make certifi actually optional +* :ghpull:`18665`: Backport PR #18639 on branch v3.3.x (nbagg: Don't close figures for bubbled events.) +* :ghpull:`18639`: nbagg: Don't close figures for bubbled events. +* :ghpull:`18640`: Backport PR #18636 on branch v3.3.x (BLD: certifi is not a run-time dependency) +* :ghpull:`18636`: BLD: certifi is not a run-time dependency +* :ghpull:`18629`: Backport PR #18621 on branch v3.3.x (Fix singleshot timers in wx.) +* :ghpull:`18621`: Fix singleshot timers in wx. +* :ghpull:`18607`: Backport PR #18604 on branch v3.3.x (Update test image to fix Ghostscript 9.53.) +* :ghpull:`18604`: Update test image to fix Ghostscript 9.53. +* :ghpull:`18584`: Fix setting 0-timeout timer with Tornado. +* :ghpull:`18550`: backport pr 18549 +* :ghpull:`18545`: Backport PR #18540 on branch v3.3.x (Call to ExitStack.push should have been ExitStack.callback.) +* :ghpull:`18549`: FIX: unit-convert pcolorargs before interpolating +* :ghpull:`18540`: Call to ExitStack.push should have been ExitStack.callback. +* :ghpull:`18533`: Correctly remove support for \stackrel. +* :ghpull:`18509`: Backport PR #18505 on branch v3.3.x (Fix depth shading when edge/facecolor is none.) +* :ghpull:`18505`: Fix depth shading when edge/facecolor is none. +* :ghpull:`18504`: Backport PR #18500 on branch v3.3.x (BUG: Fix all-masked imshow) +* :ghpull:`18500`: BUG: Fix all-masked imshow +* :ghpull:`18476`: CI: skip qt, cairo, pygobject related installs on OSX on travis +* :ghpull:`18134`: Build on xcode9 + +Issues (14): + +* :ghissue:`18885`: 3D Scatter Plot with Colorbar is not saved correctly with savefig +* :ghissue:`18922`: pyplot.xticks(): Font property specification is not effective except 1st tick label. +* :ghissue:`18481`: "%matplotlib notebook" not working in firefox with matplotlib 3.3.1 +* :ghissue:`18595`: Getting internal "MatplotlibDeprecationWarning: shading='flat' ..." +* :ghissue:`18743`: from mpl 3.2.2 to 3.3.0 enormous increase in creation time +* :ghissue:`18317`: pcolormesh: shading='nearest' and non-monotonic coordinates +* :ghissue:`18758`: Using Axis.grid(visible=True) results in TypeError for multiple values for keyword argument +* :ghissue:`18638`: ``matplotlib>=3.3.2`` breaks ``ipywidgets.interact`` +* :ghissue:`18337`: Error installing matplotlib-3.3.1 using pip due to old version of certifi on conda environment +* :ghissue:`18620`: wx backend assertion error with fig.canvas.timer.start() +* :ghissue:`18551`: test_transparent_markers[pdf] is broken on v3.3.x Travis macOS +* :ghissue:`18580`: Animation freezes in Jupyter notebook +* :ghissue:`18547`: pcolormesh x-axis with datetime broken for nearest shading +* :ghissue:`18539`: Error in Axes.redraw_in_frame in use of ExitStack: push() takes 2 positional arguments but 3 were given Previous GitHub Stats diff --git a/doc/users/prev_whats_new/github_stats_3.3.2.rst b/doc/users/prev_whats_new/github_stats_3.3.2.rst new file mode 100644 index 000000000000..8f9bb9a6eceb --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.3.2.rst @@ -0,0 +1,89 @@ +.. _github-stats-3-3-2: + +GitHub Stats for Matplotlib 3.3.2 +================================= + +GitHub stats for 2020/08/14 - 2020/09/15 (tag: v3.3.1) + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 15 issues and merged 39 pull requests. +The full list can be seen `on GitHub `__ + +The following 13 authors contributed 61 commits. + +* Antony Lee +* Bruno Beltran +* David Stansby +* David Young +* Elliott Sales de Andrade +* Greg Lucas +* Jody Klymak +* johnthagen +* Jouni K. Seppänen +* Richard Sheridan +* Ryan May +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (39): + +* :ghpull:`18488`: Backport PR #18483 on branch v3.3.x (DOC: reword non-monotonic cell center warning) +* :ghpull:`18483`: DOC: reword non-monotonic cell center warning +* :ghpull:`18485`: Backport PR #18475 on branch v3.3.x (BF: ensure exception caught if no kpeswitch) +* :ghpull:`18482`: Backport PR #18398 on branch v3.3.x (Warn on non-increasing/decreasing pcolor coords) +* :ghpull:`18484`: Backport PR #18458: Fix huge imshow range +* :ghpull:`18475`: BF: ensure exception caught if no kpeswitch +* :ghpull:`18458`: Fix huge imshow range +* :ghpull:`18398`: Warn on non-increasing/decreasing pcolor coords +* :ghpull:`18479`: Nbagg backports +* :ghpull:`18454`: nbagg: Use OutputArea event to trigger figure close. +* :ghpull:`18469`: Backport PR #18464 on branch v3.3.x (Remove extra stickies in barstacked histogram.) +* :ghpull:`18464`: Remove extra stickies in barstacked histogram. +* :ghpull:`18459`: Backport PR #18393 on branch v3.3.x (Fix Axis scale on twinned Axes.) +* :ghpull:`18393`: Fix Axis scale on twinned Axes. +* :ghpull:`18441`: Backport PR #18395: TkAgg bugfix: deselect buttons that are not the current _Mode +* :ghpull:`18395`: TkAgg bugfix: deselect buttons that are not the current _Mode +* :ghpull:`18380`: Backport PR #18374 on branch v3.3.x (FIX: make _reshape_2D accept pandas df with string indices) +* :ghpull:`18374`: FIX: make _reshape_2D accept pandas df with string indices +* :ghpull:`18376`: Backport PR #18298 on branch v3.3.x (Include license files in built distribution) +* :ghpull:`18375`: Backport PR #18293 on branch v3.3.x (Fix scatter3d color/linewidth re-projection) +* :ghpull:`18298`: Include license files in built distribution +* :ghpull:`18293`: Fix scatter3d color/linewidth re-projection +* :ghpull:`18361`: nbagg: Store DPI ratio on figure instead of window. +* :ghpull:`18354`: Backport PR #18352 on branch v3.3.x (Avoid triggering backend resolution during qt initial import.) +* :ghpull:`18352`: Avoid triggering backend resolution during qt initial import. +* :ghpull:`18335`: Backport PR #18322 on branch v3.3.x (Disable FH4 so that we don't require VCRUNTIME140_1.dll.) +* :ghpull:`18322`: Disable FH4 so that we don't require VCRUNTIME140_1.dll. +* :ghpull:`18333`: Backport PR #18328 on branch v3.3.x (Add missing check for None in Qt toolmanager.) +* :ghpull:`18328`: Add missing check for None in Qt toolmanager. +* :ghpull:`18309`: Backport PR #18304 on branch v3.3.x (Fix canvas redraws during motion in figures with a Button or TextBox) +* :ghpull:`18304`: Fix canvas redraws during motion in figures with a Button or TextBox +* :ghpull:`18297`: Backport PR #18288 on branch v3.3.x (FIX: check if axes is off page before repositioning title) +* :ghpull:`18288`: FIX: check if axes is off page before repositioning title +* :ghpull:`18269`: Backport PR #18266 on branch v3.3.x (Fix Path.get_extents for empty paths.) +* :ghpull:`18266`: Fix Path.get_extents for empty paths. +* :ghpull:`18263`: Backport PR #18260 on branch v3.3.x (Add parent widget to IntVar) +* :ghpull:`18260`: Add parent widget to IntVar +* :ghpull:`18253`: Backport PR #18245 on branch v3.3.x +* :ghpull:`18245`: MNT: do a better job guessing the GUI framework in use + +Issues (15): + +* :ghissue:`18415`: imshow with LogNorm crashes with certain inputs +* :ghissue:`18447`: nbagg: Closing a figure from the notebook does not close the python figure +* :ghissue:`18470`: interactive plots slow with matplotlib 3.3.1 +* :ghissue:`18457`: Incorrect log y-scale for histogram with partitioned and barstacked data +* :ghissue:`18385`: twinx not respecting log-scale +* :ghissue:`18371`: Plotting a pandas DataFrame with string MultiIndex +* :ghissue:`18296`: LICENSE file(s) not included in published PyPI package +* :ghissue:`18287`: scatter3D assigns wrong color to points for some plot orientations +* :ghissue:`18292`: ImportError: DLL load failed with Matplotlib 3.3.1 on Windows +* :ghissue:`18327`: Tool Manager: adding buttons to toolbar fails with matplotlib version 3.3.1 using Qt backend +* :ghissue:`18324`: Poor UI responsiveness of 3.3.1 compared with 3.2.2 for interactive mode UI using widgets +* :ghissue:`18303`: Canvas redraws during any motion when Button is present +* :ghissue:`18283`: Automatic title placement wrong if parent axes is off the page +* :ghissue:`18254`: scatter(..., marker='') raises on drawing with mpl3.3.1 +* :ghissue:`18259`: New IntVar needs a parent widget diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 340eaeb83910..1c47973f1505 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -762,7 +762,11 @@ def is_url(https://github.com/filename): @functools.lru_cache() def _get_ssl_context(): - import certifi + try: + import certifi + except ImportError: + _log.debug("Could not import certifi.") + return None import ssl return ssl.create_default_context(cafile=certifi.where()) @@ -771,7 +775,12 @@ def _get_ssl_context(): def _open_file_or_url(https://github.com/fname): if not isinstance(fname, Path) and is_url(https://github.com/fname): import urllib.request - with urllib.request.urlopen(fname, context=_get_ssl_context()) as f: + ssl_ctx = _get_ssl_context() + if ssl_ctx is None: + _log.debug( + "Could not get certifi ssl context, https may not work." + ) + with urllib.request.urlopen(fname, context=ssl_ctx) as f: yield (line.decode('utf-8') for line in f) else: fname = os.path.expanduser(fname) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 7018bbb3626d..72ca0e6266d4 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5535,8 +5535,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, self.add_image(im) return im - @staticmethod - def _pcolorargs(funcname, *args, shading='flat'): + def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): # - create X and Y if not present; # - reshape X and Y as needed if they are 1-D; # - check for proper sizes based on `shading` kwarg; @@ -5567,6 +5566,10 @@ def _pcolorargs(funcname, *args, shading='flat'): # Check x and y for bad data... C = np.asanyarray(args[2]) X, Y = [cbook.safe_masked_invalid(a) for a in args[:2]] + # unit conversion allows e.g. datetime objects as axis values + self._process_unit_info(xdata=X, ydata=Y, kwargs=kwargs) + X = self.convert_xunits(X) + Y = self.convert_yunits(Y) if funcname == 'pcolormesh': if np.ma.is_masked(X) or np.ma.is_masked(Y): raise ValueError( @@ -5815,14 +5818,10 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, if shading is None: shading = rcParams['pcolor.shading'] shading = shading.lower() - X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading) + X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading, + kwargs=kwargs) Ny, Nx = X.shape - # unit conversion allows e.g. datetime objects as axis values - self._process_unit_info(xdata=X, ydata=Y, kwargs=kwargs) - X = self.convert_xunits(X) - Y = self.convert_yunits(Y) - # convert to MA, if necessary. C = ma.asarray(C) X = ma.asarray(X) @@ -6091,14 +6090,10 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, kwargs.setdefault('edgecolors', 'None') X, Y, C, shading = self._pcolorargs('pcolormesh', *args, - shading=shading) + shading=shading, kwargs=kwargs) Ny, Nx = X.shape X = X.ravel() Y = Y.ravel() - # unit conversion allows e.g. datetime objects as axis values - self._process_unit_info(xdata=X, ydata=Y, kwargs=kwargs) - X = self.convert_xunits(X) - Y = self.convert_yunits(Y) # convert to one dimensional arrays C = C.ravel() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 404379adcecc..278dab735e6f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2774,7 +2774,7 @@ def redraw_in_frame(self): with ExitStack() as stack: for artist in [*self._get_axis_list(), self.title, self._left_title, self._right_title]: - stack.push(artist.set_visible, artist.get_visible()) + stack.callback(artist.set_visible, artist.get_visible()) artist.set_visible(False) self.draw(self.figure._cachedRenderer) @@ -2885,8 +2885,6 @@ def grid(self, b=None, which='major', axis='both', **kwargs): use `.set_axisbelow` or, for more control, call the `~.Artist.set_zorder` method of each axis. """ - if len(kwargs): - b = True cbook._check_in_list(['x', 'y', 'both'], axis=axis) if axis in ['x', 'both']: self.xaxis.grid(b, which=which, **kwargs) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index f970a4452660..10d132f03694 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -778,10 +778,10 @@ def cla(self): self.callbacks = cbook.CallbackRegistry() # whether the grids are on - self._gridOnMajor = ( + self._major_tick_kw['gridOn'] = ( mpl.rcParams['axes.grid'] and mpl.rcParams['axes.grid.which'] in ('both', 'major')) - self._gridOnMinor = ( + self._minor_tick_kw['gridOn'] = ( mpl.rcParams['axes.grid'] and mpl.rcParams['axes.grid.which'] in ('both', 'minor')) @@ -1381,7 +1381,6 @@ def get_major_ticks(self, numticks=None): # Update the new tick label properties from the old. tick = self._get_tick(major=True) self.majorTicks.append(tick) - tick.gridline.set_visible(self._gridOnMajor) self._copy_tick_props(self.majorTicks[0], tick) return self.majorTicks[:numticks] @@ -1395,7 +1394,6 @@ def get_minor_ticks(self, numticks=None): # Update the new tick label properties from the old. tick = self._get_tick(major=False) self.minorTicks.append(tick) - tick.gridline.set_visible(self._gridOnMinor) self._copy_tick_props(self.minorTicks[0], tick) return self.minorTicks[:numticks] @@ -1420,32 +1418,37 @@ def grid(self, b=None, which='major', **kwargs): Define the line properties of the grid, e.g.:: grid(color='r', linestyle='-', linewidth=2) - """ - if len(kwargs): - if not b and b is not None: # something false-like but not None + if b is not None: + if 'visible' in kwargs and bool(b) != bool(kwargs['visible']): + raise ValueError( + "'b' and 'visible' specify inconsistent grid visibilities") + if kwargs and not b: # something false-like but not None cbook._warn_external('First parameter to grid() is false, ' 'but line properties are supplied. The ' 'grid will be enabled.') - b = True + b = True which = which.lower() cbook._check_in_list(['major', 'minor', 'both'], which=which) gridkw = {'grid_' + item[0]: item[1] for item in kwargs.items()} + if 'grid_visible' in gridkw: + forced_visibility = True + gridkw['gridOn'] = gridkw.pop('grid_visible') + else: + forced_visibility = False if which in ['minor', 'both']: - if b is None: - self._gridOnMinor = not self._gridOnMinor - else: - self._gridOnMinor = b - self.set_tick_params(which='minor', gridOn=self._gridOnMinor, - **gridkw) + if b is None and not forced_visibility: + gridkw['gridOn'] = not self._minor_tick_kw['gridOn'] + elif b is not None: + gridkw['gridOn'] = b + self.set_tick_params(which='minor', **gridkw) if which in ['major', 'both']: - if b is None: - self._gridOnMajor = not self._gridOnMajor - else: - self._gridOnMajor = b - self.set_tick_params(which='major', gridOn=self._gridOnMajor, - **gridkw) + if b is None and not forced_visibility: + gridkw['gridOn'] = not self._major_tick_kw['gridOn'] + elif b is not None: + gridkw['gridOn'] = b + self.set_tick_params(which='major', **gridkw) self.stale = True def update_units(self, data): diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 2155381fbd77..5377f3dc266b 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -517,7 +517,7 @@ def _timer_start(self): else: self._timer = tornado.ioloop.PeriodicCallback( self._on_timer, - self.interval) + max(self.interval, 1e-6)) self._timer.start() def _timer_stop(self): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 460057cc30e6..0b94f2116ada 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -87,9 +87,6 @@ def _timer_set_interval(self): if self._timer.IsRunning(): self._timer_start() # Restart with new interval. - def _timer_set_single_shot(self): - self._timer.Start() - class RendererWx(RendererBase): """ diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 9e7959ec30e2..a3a8f7abc54b 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -169,7 +169,17 @@ mpl.figure.prototype._init_canvas = function () { 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;' ); - var resizeObserver = new ResizeObserver(function (entries) { + // Apply a ponyfill if ResizeObserver is not implemented by browser. + if (this.ResizeObserver === undefined) { + if (window.ResizeObserver !== undefined) { + this.ResizeObserver = window.ResizeObserver; + } else { + var obs = _JSXTOOLS_RESIZE_OBSERVER({}); + this.ResizeObserver = obs.ResizeObserver; + } + } + + this.resizeObserverInstance = new this.ResizeObserver(function (entries) { var nentries = entries.length; for (var i = 0; i < nentries; i++) { var entry = entries[i]; @@ -222,7 +232,7 @@ mpl.figure.prototype._init_canvas = function () { } } }); - resizeObserver.observe(canvas_div); + this.resizeObserverInstance.observe(canvas_div); function on_mouse_event_closure(name) { return function (event) { @@ -669,3 +679,7 @@ mpl.figure.prototype.toolbar_button_onclick = function (name) { mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) { this.message.textContent = tooltip; }; + +///////////////// REMAINING CONTENT GENERATED BY embed_js.py ///////////////// +// prettier-ignore +var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError("Constructor requires 'new' operator");i.set(this,e)}function h(){throw new TypeError("Function is not a constructor")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line diff --git a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js index 0f538979d19d..9c4ff87b5f7d 100644 --- a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js @@ -48,7 +48,7 @@ mpl.mpl_figure_comm = function (comm, msg) { console.error('Failed to find cell for figure', id, fig); return; } - fig.cell_info[0].output_area.element.one( + fig.cell_info[0].output_area.element.on( 'cleared', { fig: fig }, fig._remove_fig_handler @@ -61,6 +61,7 @@ mpl.figure.prototype.handle_close = function (fig, msg) { 'cleared', fig._remove_fig_handler ); + fig.resizeObserverInstance.unobserve(fig.canvas_div); // Update the output cell to use the data from the current canvas. fig.push_to_output(); @@ -181,6 +182,10 @@ mpl.figure.prototype._init_toolbar = function () { mpl.figure.prototype._remove_fig_handler = function (event) { var fig = event.data.fig; + if (event.target !== this) { + // Ignore bubbled events from children. + return; + } fig.close_ws(fig, {}); }; diff --git a/lib/matplotlib/backends/web_backend/package.json b/lib/matplotlib/backends/web_backend/package.json index e2a4009a971b..95bd8fdf54e6 100644 --- a/lib/matplotlib/backends/web_backend/package.json +++ b/lib/matplotlib/backends/web_backend/package.json @@ -11,5 +11,8 @@ "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}\"" + }, + "dependencies": { + "@jsxtools/resize-observer": "^1.0.4" } } diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 0dc84edc2acc..4b2f86003eaa 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -788,6 +788,9 @@ def _add_solids(self, X, Y, C): Draw the colors using `~.axes.Axes.pcolormesh`; optionally add separators. """ + if C.shape[0] == Y.shape[0]: + # trim the last one to be compatible with old behavior. + C = C[:-1] if self.orientation == 'vertical': args = (X, Y, C) else: diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 4b56f07b9525..4a2e7374efec 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -285,25 +285,6 @@ def get_epoch(): return _epoch -def _to_ordinalf(dt): - """ - Convert :mod:`datetime` or :mod:`date` to the Gregorian date as UTC float - days, preserving hours, minutes, seconds and microseconds. Return value - is a `float`. - """ - # Convert to UTC - tzi = getattr(dt, 'tzinfo', None) - if tzi is not None: - dt = dt.astimezone(UTC) - dt = dt.replace(tzinfo=None) - dt64 = np.datetime64(dt) - return _dt64_to_ordinalf(dt64) - - -# a version of _to_ordinalf that can operate on numpy arrays -_to_ordinalf_np_vectorized = np.vectorize(_to_ordinalf) - - def _dt64_to_ordinalf(d): """ Convert `numpy.datetime64` or an ndarray of those types to Gregorian @@ -428,20 +409,29 @@ def date2num(d): if hasattr(d, "values"): # this unpacks pandas series or dataframes... d = d.values - if not np.iterable(d): - if (isinstance(d, np.datetime64) or - (isinstance(d, np.ndarray) and - np.issubdtype(d.dtype, np.datetime64))): - return _dt64_to_ordinalf(d) - return _to_ordinalf(d) - else: - d = np.asarray(d) - if np.issubdtype(d.dtype, np.datetime64): - return _dt64_to_ordinalf(d) + # make an iterable, but save state to unpack later: + iterable = np.iterable(d) + if not iterable: + d = [d] + + d = np.asarray(d) + # convert to datetime64 arrays, if not already: + if not np.issubdtype(d.dtype, np.datetime64): + # datetime arrays if not d.size: + # deals with an empty array... return d - return _to_ordinalf_np_vectorized(d) + tzi = getattr(d[0], 'tzinfo', None) + if tzi is not None: + # make datetime naive: + d = [dt.astimezone(UTC).replace(tzinfo=None) for dt in d] + d = np.asarray(d) + d = d.astype('datetime64[us]') + + d = _dt64_to_ordinalf(d) + + return d if iterable else d[0] def julian2num(j): diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 14866cc1012c..7d15e9f1c396 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -474,8 +474,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # do not run the vmin/vmax through the same pipeline we can # have values close or equal to the boundaries end up on the # wrong side. - vrange = np.array([self.norm.vmin, self.norm.vmax], - dtype=scaled_dtype) + vmin, vmax = self.norm.vmin, self.norm.vmax + if vmin is np.ma.masked: + vmin, vmax = a_min, a_max + vrange = np.array([vmin, vmax], dtype=scaled_dtype) A_scaled -= a_min vrange -= a_min @@ -1479,8 +1481,12 @@ def imread(fname, format=None): if len(parsed.scheme) > 1: # Pillow doesn't handle URLs directly. # hide imports to speed initial import on systems with slow linkers from urllib import request - with request.urlopen(fname, - context=mpl._get_ssl_context()) as response: + ssl_ctx = mpl._get_ssl_context() + if ssl_ctx is None: + _log.debug( + "Could not get certifi ssl context, https may not work." + ) + with request.urlopen(fname, context=ssl_ctx) as response: import io try: response.seek(0) diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 1b46fc6dcd7d..d174aaf556f3 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -2396,7 +2396,6 @@ def __init__(self): p.accentprefixed = Forward() p.space = Forward() p.sqrt = Forward() - p.stackrel = Forward() p.start_group = Forward() p.subsuper = Forward() p.subsuperop = Forward() @@ -2481,12 +2480,6 @@ def __init__(self): | Error(r"Expected \dfrac{num}{den}")) ) - p.stackrel <<= Group( - Suppress(Literal(r"\stackrel")) - - ((p.required_group + p.required_group) - | Error(r"Expected \stackrel{num}{den}")) - ) - p.binom <<= Group( Suppress(Literal(r"\binom")) - ((p.required_group + p.required_group) @@ -2543,7 +2536,6 @@ def __init__(self): | p.group | p.frac | p.dfrac - | p.stackrel | p.binom | p.genfrac | p.sqrt diff --git a/lib/matplotlib/tests/baseline_images/test_axes/transparent_markers.pdf b/lib/matplotlib/tests/baseline_images/test_axes/transparent_markers.pdf index d1245169e994..305bcb90ab99 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/transparent_markers.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/transparent_markers.pdf differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 41df0f502347..57beec025aa8 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1188,6 +1188,22 @@ def test_pcolornearest(fig_test, fig_ref): ax.pcolormesh(x2, y2, Z, shading='nearest') +@check_figures_equal(extensions=["png"]) +def test_pcolornearestunits(fig_test, fig_ref): + ax = fig_test.subplots() + x = [datetime.datetime.fromtimestamp(x * 3600) for x in range(10)] + y = np.arange(0, 3) + np.random.seed(19680801) + Z = np.random.randn(2, 9) + ax.pcolormesh(x, y, Z, shading='flat') + + ax = fig_ref.subplots() + # specify the centers + x2 = [datetime.datetime.fromtimestamp((x + 0.5) * 3600) for x in range(9)] + y2 = y[:-1] + np.diff(y) / 2 + ax.pcolormesh(x2, y2, Z, shading='nearest') + + @check_figures_equal(extensions=["png"]) def test_pcolordropdata(fig_test, fig_ref): ax = fig_test.subplots() @@ -4294,26 +4310,35 @@ def test_twin_spines_on_top(): ax2.fill_between("i", "j", color='#7FC97F', alpha=.5, data=data) -def test_rcparam_grid_minor(): - orig_grid = matplotlib.rcParams['axes.grid'] - orig_locator = matplotlib.rcParams['axes.grid.which'] - - matplotlib.rcParams['axes.grid'] = True - - values = ( - (('both'), (True, True)), - (('major'), (True, False)), - (('minor'), (False, True)) - ) +@pytest.mark.parametrize("grid_which, major_visible, minor_visible", [ + ("both", True, True), + ("major", True, False), + ("minor", False, True), +]) +def test_rcparam_grid_minor(grid_which, major_visible, minor_visible): + mpl.rcParams.update({"axes.grid": True, "axes.grid.which": grid_which}) + fig, ax = plt.subplots() + fig.canvas.draw() + assert all(tick.gridline.get_visible() == major_visible + for tick in ax.xaxis.majorTicks) + assert all(tick.gridline.get_visible() == minor_visible + for tick in ax.xaxis.minorTicks) - for locator, result in values: - matplotlib.rcParams['axes.grid.which'] = locator - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - assert (ax.xaxis._gridOnMajor, ax.xaxis._gridOnMinor) == result - matplotlib.rcParams['axes.grid'] = orig_grid - matplotlib.rcParams['axes.grid.which'] = orig_locator +def test_grid(): + fig, ax = plt.subplots() + ax.grid() + fig.canvas.draw() + assert ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid(visible=False) + fig.canvas.draw() + assert not ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid(visible=True) + fig.canvas.draw() + assert ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid() + fig.canvas.draw() + assert not ax.xaxis.majorTicks[0].gridline.get_visible() def test_vline_limit(): @@ -6350,6 +6375,13 @@ def test_bbox_aspect_axes_init(): assert_allclose(sizes, sizes[0]) +def test_redraw_in_frame(): + fig, ax = plt.subplots(1, 1) + ax.plot([1, 2, 3]) + fig.canvas.draw() + ax.redraw_in_frame() + + def test_invisible_axes(): # invisible axes should not respond to events... fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 87e3c121d4c0..6ffb2e2d0554 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -846,6 +846,14 @@ def test_mask_image(): ax2.imshow(A, interpolation='nearest') +def test_mask_image_all(): + # Test behavior with an image that is entirely masked does not warn + data = np.full((2, 2), np.nan) + fig, ax = plt.subplots() + ax.imshow(data) + fig.canvas.draw_idle() # would emit a warning + + @image_comparison(['imshow_endianess.png'], remove_text=True) def test_imshow_endianess(): x = np.arange(10) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 6bc84b19f1eb..91a72a648441 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -214,8 +214,6 @@ def test_fontinfo(): (r'$\hspace{foo}$', r'Expected \hspace{n}'), (r'$\frac$', r'Expected \frac{num}{den}'), (r'$\frac{}{}$', r'Expected \frac{num}{den}'), - (r'$\stackrel$', r'Expected \stackrel{num}{den}'), - (r'$\stackrel{}{}$', r'Expected \stackrel{num}{den}'), (r'$\binom$', r'Expected \binom{num}{den}'), (r'$\binom{}{}$', r'Expected \binom{num}{den}'), (r'$\genfrac$', @@ -238,8 +236,6 @@ def test_fontinfo(): 'hspace with invalid value', 'frac without parameters', 'frac with empty parameters', - 'stackrel without parameters', - 'stackrel with empty parameters', 'binom without parameters', 'binom with empty parameters', 'genfrac without parameters', diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 70855e6d879f..8d91b1318d88 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -8,9 +8,11 @@ import matplotlib as mpl from matplotlib.backend_bases import MouseEvent +from matplotlib.font_manager import FontProperties import matplotlib.patches as mpatches import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.text import Text needs_usetex = pytest.mark.skipif( @@ -689,3 +691,13 @@ def test_fontproperties_kwarg_precedence(): text2 = plt.ylabel("counts", size=40.0, fontproperties='Times New Roman') assert text1.get_size() == 40.0 assert text2.get_size() == 40.0 + + +def test_update_mutate_input(): + inp = dict(fontproperties=FontProperties(weight="bold"), + bbox=None) + cache = dict(inp) + t = Text() + t.update(inp) + assert inp['fontproperties'] == cache['fontproperties'] + assert inp['bbox'] == cache['bbox'] diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 611b5cf968d8..9033e30d4858 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1347,3 +1347,13 @@ def test_bad_locator_subs(sub): ll = mticker.LogLocator() with pytest.raises(ValueError): ll.subs(sub) + + +@pytest.mark.parametrize('numticks', [1, 2, 3, 9]) +@pytest.mark.style('default') +def test_small_range_loglocator(numticks): + ll = mticker.LogLocator() + ll.set_params(numticks=numticks) + for top in [5, 7, 9, 11, 15, 50, 100, 1000]: + ticks = ll.tick_values(.5, top) + assert (np.diff(np.log10(ll.tick_values(6, 150))) == 1).all() diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 3b30a4cfb502..efe4450f4fba 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -167,6 +167,8 @@ def __init__(self, def update(self, kwargs): # docstring inherited + # make a copy so we do not mutate user input! + kwargs = dict(kwargs) sentinel = object() # bbox can be None, so use another sentinel. # Update fontproperties first, as it has lowest priority. fontproperties = kwargs.pop("fontproperties", sentinel) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index ced002157637..67931617423f 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2500,6 +2500,13 @@ def tick_values(self, vmin, vmax): if mpl.rcParams['_internal.classic_mode'] else (numdec + 1) // numticks + 1) + # if we have decided that the stride is as big or bigger than + # the range, clip the stride back to the available range - 1 + # with a floor of 1. This prevents getting axis with only 1 tick + # visible. + if stride >= numdec: + stride = max(1, numdec - 1) + # Does subs include anything other than 1? Essentially a hack to know # whether we're a major or a minor locator. have_subs = len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0) diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index db89bd434eb8..d3e32477adf2 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -439,9 +439,9 @@ def get_gridlines(self, which="major", axis="both"): if axis in ["both", "y"]: x1, x2 = self.axes.get_xlim() locs = [] - if self.axes.yaxis._gridOnMajor: + if self.axes.yaxis._major_tick_kw["gridOn"]: locs.extend(self.axes.yaxis.major.locator()) - if self.axes.yaxis._gridOnMinor: + if self.axes.yaxis._minor_tick_kw["gridOn"]: locs.extend(self.axes.yaxis.minor.locator()) for y in locs: @@ -533,17 +533,17 @@ def grid(self, b=None, which='major', axis="both", **kwargs): """ Toggle the gridlines, and optionally set the properties of the lines. """ - # their are some discrepancy between the behavior of grid in - # axes_grid and the original mpl's grid, because axes_grid - # explicitly set the visibility of the gridlines. + # There are some discrepancies in the behavior of grid() between + # axes_grid and Matplotlib, because axes_grid explicitly sets the + # visibility of the gridlines. super().grid(b, which=which, axis=axis, **kwargs) if not self._axisline_on: return if b is None: - b = (self.axes.xaxis._gridOnMinor - or self.axes.xaxis._gridOnMajor - or self.axes.yaxis._gridOnMinor - or self.axes.yaxis._gridOnMajor) + b = (self.axes.xaxis._minor_tick_kw["gridOn"] + or self.axes.xaxis._major_tick_kw["gridOn"] + or self.axes.yaxis._minor_tick_kw["gridOn"] + or self.axes.yaxis._major_tick_kw["gridOn"]) self.gridlines.set(which=which, axis=axis, visible=b) self.gridlines.set(**kwargs) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 678cd0d7b872..1c6f9d0ba72d 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -264,6 +264,8 @@ def do_3d_projection(self, renderer): """ Project the points according to renderer matrix. """ + # see _update_scalarmappable docstring for why this must be here + _update_scalarmappable(self) xyslist = [ proj3d.proj_trans_points(points, renderer.M) for points in self._segments3d] @@ -418,6 +420,8 @@ def set_3d_properties(self, zs, zdir): self.stale = True def do_3d_projection(self, renderer): + # see _update_scalarmappable docstring for why this must be here + _update_scalarmappable(self) xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) @@ -486,6 +490,8 @@ def set_3d_properties(self, zs, zdir): self.stale = True def do_3d_projection(self, renderer): + # see _update_scalarmappable docstring for why this must be here + _update_scalarmappable(self) xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) @@ -528,6 +534,77 @@ def do_3d_projection(self, renderer): return np.min(vzs) if vzs.size else np.nan +def _update_scalarmappable(sm): + """ + Update a 3D ScalarMappable. + + With ScalarMappable objects if the data, colormap, or norm are + changed, we need to update the computed colors. This is handled + by the base class method update_scalarmappable. This method works + by, detecting if work needs to be done, and if so stashing it on + the ``self._facecolors`` attribute. + + With 3D collections we internally sort the components so that + things that should be "in front" are rendered later to simulate + having a z-buffer (in addition to doing the projections). This is + handled in the ``do_3d_projection`` methods which are called from the + draw method of the 3D Axes. These methods: + + - do the projection from 3D -> 2D + - internally sort based on depth + - stash the results of the above in the 2D analogs of state + - return the z-depth of the whole artist + + the last step is so that we can, at the Axes level, sort the children by + depth. + + The base `draw` method of the 2D artists unconditionally calls + update_scalarmappable and rely on the method's internal caching logic to + lazily evaluate. + + These things together mean you can have the sequence of events: + + - we create the artist, do the color mapping and stash the results + in a 3D specific state. + - change something about the ScalarMappable that marks it as in + need of an update (`ScalarMappable.changed` and friends). + - We call do_3d_projection and shuffle the stashed colors into the + 2D version of face colors + - the draw method calls the update_scalarmappable method which + overwrites our shuffled colors + - we get a render that is wrong + - if we re-render (either with a second save or implicitly via + tight_layout / constrained_layout / bbox_inches='tight' (ex via + inline's defaults)) we again shuffle the 3D colors + - because the CM is not marked as changed update_scalarmappable is + a no-op and we get a correct looking render. + + This function is an internal helper to: + + - sort out if we need to do the color mapping at all (has data!) + - sort out if update_scalarmappable is going to be a no-op + - copy the data over from the 2D -> 3D version + + This must be called first thing in do_3d_projection to make sure that + the correct colors get shuffled. + + Parameters + ---------- + sm : ScalarMappable + The ScalarMappable to update and stash the 3D data from + + """ + if sm._A is None: + return + copy_state = sm._update_dict['array'] + ret = sm.update_scalarmappable() + if copy_state: + if sm._is_filled: + sm._facecolor3d = sm._facecolors + elif sm._is_stroked: + sm._edgecolor3d = sm._edgecolors + + def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): """ Convert a :class:`~matplotlib.collections.PatchCollection` into a @@ -650,8 +727,8 @@ def set_3d_properties(self): self.update_scalarmappable() self._sort_zpos = None self.set_zsort('average') - self._facecolors3d = PolyCollection.get_facecolor(self) - self._edgecolors3d = PolyCollection.get_edgecolor(self) + self._facecolor3d = PolyCollection.get_facecolor(self) + self._edgecolor3d = PolyCollection.get_edgecolor(self) self._alpha3d = PolyCollection.get_alpha(self) self.stale = True @@ -664,17 +741,15 @@ def do_3d_projection(self, renderer): """ Perform the 3D projection for this object. """ - # FIXME: This may no longer be needed? - if self._A is not None: - self.update_scalarmappable() - self._facecolors3d = self._facecolors + # see _update_scalarmappable docstring for why this must be here + _update_scalarmappable(self) txs, tys, tzs = proj3d._proj_transform_vec(self._vec, renderer.M) xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] # This extra fuss is to re-order face / edge colors - cface = self._facecolors3d - cedge = self._edgecolors3d + cface = self._facecolor3d + cedge = self._edgecolor3d if len(cface) != len(xyzlist): cface = cface.repeat(len(xyzlist), axis=0) if len(cedge) != len(xyzlist): @@ -699,8 +774,8 @@ def do_3d_projection(self, renderer): else: PolyCollection.set_verts(self, segments_2d, self._closed) - if len(self._edgecolors3d) != len(cface): - self._edgecolors2d = self._edgecolors3d + if len(self._edgecolor3d) != len(cface): + self._edgecolors2d = self._edgecolor3d # Return zorder value if self._sort_zpos is not None: @@ -717,23 +792,23 @@ def do_3d_projection(self, renderer): def set_facecolor(self, colors): PolyCollection.set_facecolor(self, colors) - self._facecolors3d = PolyCollection.get_facecolor(self) + self._facecolor3d = PolyCollection.get_facecolor(self) def set_edgecolor(self, colors): PolyCollection.set_edgecolor(self, colors) - self._edgecolors3d = PolyCollection.get_edgecolor(self) + self._edgecolor3d = PolyCollection.get_edgecolor(self) def set_alpha(self, alpha): # docstring inherited artist.Artist.set_alpha(self, alpha) try: - self._facecolors3d = mcolors.to_rgba_array( - self._facecolors3d, self._alpha) + self._facecolor3d = mcolors.to_rgba_array( + self._facecolor3d, self._alpha) except (AttributeError, TypeError, IndexError): pass try: self._edgecolors = mcolors.to_rgba_array( - self._edgecolors3d, self._alpha) + self._edgecolor3d, self._alpha) except (AttributeError, TypeError, IndexError): pass self.stale = True @@ -803,7 +878,7 @@ def _zalpha(colors, zs): # in all three dimensions. Otherwise, at certain orientations, # the min and max zs are very close together. # Should really normalize against the viewing depth. - if len(zs) == 0: + if len(colors) == 0 or len(zs) == 0: return np.zeros((0, 4)) norm = Normalize(min(zs), max(zs)) sats = 1 - norm(zs) * 0.7 diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 9c63dc8f9fa8..999dafdc92a9 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -106,7 +106,7 @@ def test_bar3d_lightsource(): # the top facecolors compared to the default, and that those colors are # precisely the colors from the colormap, due to the illumination parallel # to the z-axis. - np.testing.assert_array_equal(color, collection._facecolors3d[1::6]) + np.testing.assert_array_equal(color, collection._facecolor3d[1::6]) @mpl3d_image_comparison(['contour3d.png']) @@ -237,8 +237,14 @@ def test_scatter3d(): def test_scatter3d_color(): fig = plt.figure() ax = fig.add_subplot(111, projection='3d') + + # Check that 'none' color works; these two should overlay to produce the + # same as setting just `color`. + ax.scatter(np.arange(10), np.arange(10), np.arange(10), + facecolor='r', edgecolor='none', marker='o') ax.scatter(np.arange(10), np.arange(10), np.arange(10), - color='r', marker='o') + facecolor='none', edgecolor='r', marker='o') + ax.scatter(np.arange(10, 20), np.arange(10, 20), np.arange(10, 20), color='b', marker='s') @@ -1081,3 +1087,25 @@ def test_colorbar_pos(): fig.canvas.draw() # check that actually on the bottom assert cbar.ax.get_position().extents[1] < 0.2 + + +@pytest.mark.style('default') +@check_figures_equal(extensions=["png"]) +def test_scalarmap_update(fig_test, fig_ref): + + x, y, z = np.array((list(itertools.product(*[np.arange(0, 5, 1), + np.arange(0, 5, 1), + np.arange(0, 5, 1)])))).T + c = x + y + + # test + ax_test = fig_test.add_subplot(111, projection='3d') + sc_test = ax_test.scatter(x, y, z, c=c, s=40, cmap='viridis') + # force a draw + fig_test.canvas.draw() + # mark it as "stale" + sc_test.changed() + + # ref + ax_ref = fig_ref.add_subplot(111, projection='3d') + sc_ref = ax_ref.scatter(x, y, z, c=c, s=40, cmap='viridis') diff --git a/setup.py b/setup.py index 81eae826373d..6e1d19d4b85c 100644 --- a/setup.py +++ b/setup.py @@ -287,7 +287,6 @@ def build_extensions(self): "numpy>=1.15", ], install_requires=[ - "certifi>=2020.06.20", "cycler>=0.10", "kiwisolver>=1.0.1", "numpy>=1.15", diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index b3683fe19011..27d707ae36b7 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -62,6 +62,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.3.2": "4030140", "v3.3.1": "3984190", "v3.3.0": "3948793", "v3.2.2": "3898017", diff --git a/tools/embed_js.py b/tools/embed_js.py new file mode 100644 index 000000000000..571bf80238e9 --- /dev/null +++ b/tools/embed_js.py @@ -0,0 +1,102 @@ +""" +Script to embed JavaScript dependencies in mpl.js. +""" + +from collections import namedtuple +from pathlib import Path +import re +import shutil +import subprocess +import sys + + +Package = namedtuple('Package', [ + # The package to embed, in some form that `npm install` can use. + 'name', + # The path to the source file within the package to embed. + 'source', + # The path to the license file within the package to embed. + 'license']) +# The list of packages to embed, in some form that `npm install` can use. +JAVASCRIPT_PACKAGES = [ + # Polyfill/ponyfill for ResizeObserver. + Package('@jsxtools/resize-observer', 'index.js', 'LICENSE.md'), +] +# This is the magic line that must exist in mpl.js, after which the embedded +# JavaScript will be appended. +MPLJS_MAGIC_HEADER = ( + "///////////////// REMAINING CONTENT GENERATED BY embed_js.py " + "/////////////////\n") + + +def safe_name(name): + """ + Make *name* safe to use as a JavaScript variable name. + """ + return '_'.join(re.split(r'[@/-]', name)).upper() + + +def prep_package(web_backend_path, pkg): + source = web_backend_path / 'node_modules' / pkg.name / pkg.source + license = web_backend_path / 'node_modules' / pkg.name / pkg.license + if not source.exists(): + # Exact version should already be saved in package.json, so we use + # --no-save here. + try: + subprocess.run(['npm', 'install', '--no-save', pkg.name], + cwd=web_backend_path) + except FileNotFoundError as err: + raise ValueError( + f'npm must be installed to fetch {pkg.name}') from err + if not source.exists(): + raise ValueError( + f'{pkg.name} package is missing source in {pkg.source}') + elif not license.exists(): + raise ValueError( + f'{pkg.name} package is missing license in {pkg.license}') + + return source, license + + +def gen_embedded_lines(pkg, source): + name = safe_name(pkg.name) + print('Embedding', source, 'as', name) + yield '// prettier-ignore\n' + for line in source.read_text().splitlines(): + yield (line.replace('module.exports=function', f'var {name}=function') + + ' // eslint-disable-line\n') + + +def build_mpljs(web_backend_path, license_path): + mpljs_path = web_backend_path / "js/mpl.js" + mpljs_orig = mpljs_path.read_text().splitlines(keepends=True) + try: + mpljs_orig = mpljs_orig[:mpljs_orig.index(MPLJS_MAGIC_HEADER) + 1] + except IndexError as err: + raise ValueError( + f'The mpl.js file *must* have the exact line: {MPLJS_MAGIC_HEADER}' + ) from err + + with mpljs_path.open('w') as mpljs: + mpljs.writelines(mpljs_orig) + + for pkg in JAVASCRIPT_PACKAGES: + source, license = prep_package(web_backend_path, pkg) + mpljs.writelines(gen_embedded_lines(pkg, source)) + + shutil.copy(license, + license_path / f'LICENSE{safe_name(pkg.name)}') + + +if __name__ == '__main__': + # Write the mpl.js file. + if len(sys.argv) > 1: + web_backend_path = Path(sys.argv[1]) + else: + web_backend_path = (Path(__file__).parent.parent / + "lib/matplotlib/backends/web_backend") + if len(sys.argv) > 2: + license_path = Path(sys.argv[2]) + else: + license_path = Path(__file__).parent.parent / "LICENSE" + build_mpljs(web_backend_path, license_path) diff --git a/tutorials/text/mathtext.py b/tutorials/text/mathtext.py index 7b5b078b4fa2..709f1df4956b 100644 --- a/tutorials/text/mathtext.py +++ b/tutorials/text/mathtext.py @@ -97,7 +97,7 @@ .. math:: - \frac{3}{4} \binom{3}{4} \stackrel{}{}{0}{}{3}{4} + \frac{3}{4} \binom{3}{4} \genfrac{}{}{0}{}{3}{4} Fractions can be arbitrarily nested::